From a68bd4b475bef5413196ea034edb053299e9020c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 24 Oct 2010 22:28:50 +0200 Subject: [PATCH 001/235] Ready for 0.3.0 development --- docs/changes.rst | 10 ++++++++++ mopidy/__init__.py | 2 +- tests/version_test.py | 7 ++++--- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index eadf8e75..cb34993e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,6 +5,16 @@ Changes This change log is used to track all major changes to Mopidy. +0.3.0 (in development) +====================== + +No description yet. + +**Changes** + +- None so far. + + 0.2.0 (2010-10-24) ================== diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 5e1b26de..fffa25c7 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -3,7 +3,7 @@ if not (2, 6) <= sys.version_info < (3,): sys.exit(u'Mopidy requires Python >= 2.6, < 3') def get_version(): - return u'0.2.0' + return u'0.3.0' class MopidyException(Exception): def __init__(self, message, *args, **kwargs): diff --git a/tests/version_test.py b/tests/version_test.py index fcc95c4c..a8bc2955 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -12,6 +12,7 @@ class VersionTest(unittest.TestCase): self.assert_(SV('0.1.0a1') < SV('0.1.0a2')) self.assert_(SV('0.1.0a2') < SV('0.1.0a3')) self.assert_(SV('0.1.0a3') < SV('0.1.0')) - self.assert_(SV('0.1.0') < SV(get_version())) - self.assert_(SV(get_version()) < SV('0.2.1')) - self.assert_(SV('0.2.0') < SV('1.0.0')) + self.assert_(SV('0.1.0') < SV('0.2.0')) + self.assert_(SV('0.1.0') < SV('1.0.0')) + self.assert_(SV('0.2.0') < SV(get_version())) + self.assert_(SV(get_version()) < SV('0.3.1')) From feff24cd2820cef136ea5cbf4fdf7880e8cd8518 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 24 Oct 2010 23:13:37 +0200 Subject: [PATCH 002/235] Update NadTalker to conform to new BaseThread interface --- mopidy/mixers/nad.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index 7a8f006e..8caa9700 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -40,7 +40,7 @@ class NadMixer(BaseMixer): super(NadMixer, self).__init__(*args, **kwargs) self._volume = None self._pipe, other_end = Pipe() - NadTalker(pipe=other_end).start() + NadTalker(self.backend.core_queue, pipe=other_end).start() def _get_volume(self): return self._volume @@ -72,8 +72,9 @@ class NadTalker(BaseThread): # Volume in range 0..VOLUME_LEVELS. :class:`None` before calibration. _nad_volume = None - def __init__(self, pipe=None): - super(NadTalker, self).__init__(name='NadTalker') + def __init__(self, core_queue, pipe=None): + super(NadTalker, self).__init__(core_queue) + self.name = u'NadTalker' self.pipe = pipe self._device = None From 119dc5d24cd34e2b3fb42b023a2f152f3ed76007 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 24 Oct 2010 23:39:19 +0200 Subject: [PATCH 003/235] Update pylintrc to work with pylint 0.21 --- pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylintrc b/pylintrc index d405a71f..3b89651f 100644 --- a/pylintrc +++ b/pylintrc @@ -19,4 +19,4 @@ # W0511 - TODO, FIXME and XXX in the code # W0613 - Unused argument %r # -disable-msg = C0103,C0111,C0112,E0102,E0202,E1101,R0201,R0801,R0903,R0904,W0141,W0142,W0401,W0511,W0613 +disable = C0103,C0111,C0112,E0102,E0202,E1101,R0201,R0801,R0903,R0904,W0141,W0142,W0401,W0511,W0613 From e984e2416859e83561f05163ca6746aaaa3bd39b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 24 Oct 2010 23:51:21 +0200 Subject: [PATCH 004/235] Fix or ignore more pylint warnings --- mopidy/frontends/lastfm.py | 4 ++-- mopidy/frontends/mpd/dispatcher.py | 2 ++ mopidy/outputs/gstreamer.py | 3 ++- mopidy/utils/process.py | 2 -- mopidy/utils/settings.py | 2 ++ 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index e91dd272..0cf534af 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -5,9 +5,9 @@ import time try: import pylast -except ImportError as e: +except ImportError as import_error: from mopidy import OptionalDependencyError - raise OptionalDependencyError(e) + raise OptionalDependencyError(import_error) from mopidy import get_version, settings, SettingsError from mopidy.frontends.base import BaseFrontend diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 2a477e1c..ab5f2e8c 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -5,9 +5,11 @@ from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError, from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers # Do not remove the following import. The protocol modules must be imported to # get them registered as request handlers. +# pylint: disable = W0611 from mopidy.frontends.mpd.protocol import (audio_output, command_list, connection, current_playlist, empty, music_db, playback, reflection, status, stickers, stored_playlists) +# pylint: enable = W0611 from mopidy.utils import flatten class MpdDispatcher(object): diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 3714fed6..52bd302d 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -78,7 +78,8 @@ class GStreamerOutput(BaseOutput): return self._send_recv({'command': 'get_position'}) def set_position(self, position): - return self._send_recv({'command': 'set_position', 'position': position}) + return self._send_recv({'command': 'set_position', + 'position': position}) def set_state(self, state): return self._send_recv({'command': 'set_state', 'state': state}) diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py index 7855d69c..c34d018c 100644 --- a/mopidy/utils/process.py +++ b/mopidy/utils/process.py @@ -3,7 +3,6 @@ import multiprocessing import multiprocessing.dummy from multiprocessing.reduction import reduce_connection import pickle -import sys from mopidy import SettingsError @@ -17,7 +16,6 @@ def unpickle_connection(pickled_connection): (func, args) = pickle.loads(pickled_connection) return func(*args) - class BaseProcess(multiprocessing.Process): def __init__(self, core_queue): super(BaseProcess, self).__init__() diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 1d3a0fa0..ac75cb70 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -23,7 +23,9 @@ class SettingsProxy(object): if not os.path.isfile(settings_file): return {} sys.path.insert(0, dotdir) + # 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): From b59e666bd53751ef8f5f438d033a264735f1f604 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 25 Oct 2010 22:01:40 +0200 Subject: [PATCH 005/235] Clearify cp_track_at_eot() a bit --- mopidy/backends/base/playback.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index c4ef5fbf..1faa5adf 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -149,10 +149,9 @@ class BasePlaybackController(object): return cp_tracks[0] if self.repeat and self.single: - return cp_tracks[ - (self.current_playlist_position) % len(cp_tracks)] + return cp_tracks[self.current_playlist_position] - if self.repeat: + if self.repeat and not self.single: return cp_tracks[ (self.current_playlist_position + 1) % len(cp_tracks)] From 40c1547c156f74c31888d6b7ed85f789ccaf00c3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 25 Oct 2010 22:03:40 +0200 Subject: [PATCH 006/235] Fix/ignore remaining pylint warnings --- mopidy/backends/base/playback.py | 6 ++++++ mopidy/backends/libspotify/session_manager.py | 5 +++++ mopidy/outputs/dummy.py | 3 +++ pylintrc | 3 ++- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 1faa5adf..b7ceeee2 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -10,6 +10,9 @@ class BasePlaybackController(object): :type backend: :class:`BaseBackend` """ + # pylint: disable = R0902 + # Too many instance attributes + #: Constant representing the paused state. PAUSED = u'paused' @@ -130,6 +133,9 @@ class BasePlaybackController(object): Not necessarily the same track as :attr:`cp_track_at_next`. """ + # pylint: disable = R0911 + # Too many return statements + cp_tracks = self.backend.current_playlist.cp_tracks if not cp_tracks: diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/libspotify/session_manager.py index 7f541236..45841350 100644 --- a/mopidy/backends/libspotify/session_manager.py +++ b/mopidy/backends/libspotify/session_manager.py @@ -11,6 +11,9 @@ from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.backends.libspotify.session_manager') +# pylint: disable = R0901 +# LibspotifySessionManager: Too many ancestors (9/7) + class LibspotifySessionManager(SpotifySessionManager, BaseThread): cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) @@ -65,6 +68,8 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread): def music_delivery(self, session, frames, frame_size, num_frames, sample_type, sample_rate, channels): """Callback used by pyspotify""" + # pylint: disable = R0913 + # Too many arguments (8/5) assert sample_type == 0, u'Expects 16-bit signed integer samples' capabilites = """ audio/x-raw-int, diff --git a/mopidy/outputs/dummy.py b/mopidy/outputs/dummy.py index fd42b38b..060ee02f 100644 --- a/mopidy/outputs/dummy.py +++ b/mopidy/outputs/dummy.py @@ -5,6 +5,9 @@ class DummyOutput(BaseOutput): Audio output used for testing. """ + # pylint: disable = R0902 + # Too many instance attributes (9/7) + #: For testing. :class:`True` if :meth:`start` has been called. start_called = False diff --git a/pylintrc b/pylintrc index 3b89651f..9fed307c 100644 --- a/pylintrc +++ b/pylintrc @@ -14,9 +14,10 @@ # R0903 - Too few public methods (%s/%s) # R0904 - Too many public methods (%s/%s) # W0141 - Used builtin function %r +# R0921 - Abstract class not referenced # W0142 - Used * or ** magic # W0401 - Wildcard import %s # W0511 - TODO, FIXME and XXX in the code # W0613 - Unused argument %r # -disable = C0103,C0111,C0112,E0102,E0202,E1101,R0201,R0801,R0903,R0904,W0141,W0142,W0401,W0511,W0613 +disable = C0103,C0111,C0112,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0401,W0511,W0613 From 20246e3fa9436457cbfe7de456c2e8943426bfbd Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 25 Oct 2010 22:04:39 +0200 Subject: [PATCH 007/235] Turn on warning for wilcard imports --- mopidy/backends/local/__init__.py | 4 +++- pylintrc | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index e5bfe8f8..04761e17 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -5,7 +5,9 @@ import os import shutil from mopidy import settings -from mopidy.backends.base import * +from mopidy.backends.base import (BaseBackend, BaseLibraryController, + BaseStoredPlaylistsController, BaseCurrentPlaylistController, + BasePlaybackController) from mopidy.models import Playlist, Track, Album from mopidy.utils.process import pickle_connection diff --git a/pylintrc b/pylintrc index 9fed307c..cd8c5a62 100644 --- a/pylintrc +++ b/pylintrc @@ -16,8 +16,7 @@ # W0141 - Used builtin function %r # R0921 - Abstract class not referenced # W0142 - Used * or ** magic -# W0401 - Wildcard import %s # W0511 - TODO, FIXME and XXX in the code # W0613 - Unused argument %r # -disable = C0103,C0111,C0112,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0401,W0511,W0613 +disable = C0103,C0111,C0112,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0511,W0613 From 515adc3be5335d3dc8baf35fa171ed75ccb5647b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 25 Oct 2010 22:06:18 +0200 Subject: [PATCH 008/235] Turn on warning for empty (not missing) docstrings --- pylintrc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pylintrc b/pylintrc index cd8c5a62..38b46afb 100644 --- a/pylintrc +++ b/pylintrc @@ -5,7 +5,6 @@ # # C0103 - Invalid name "%s" (should match %s) # C0111 - Missing docstring -# C0112 - Empty docstring # E0102 - %s already defined line %s # E0202 - An attribute inherited from %s hide this method # E1101 - %s %r has no %r member @@ -19,4 +18,4 @@ # W0511 - TODO, FIXME and XXX in the code # W0613 - Unused argument %r # -disable = C0103,C0111,C0112,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0511,W0613 +disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0511,W0613 From 3ebccc50c9b65195f40ed5c9fc54f5ea4c02ac48 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 25 Oct 2010 22:06:49 +0200 Subject: [PATCH 009/235] Explain why we ignore pylint errors --- pylintrc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pylintrc b/pylintrc index 38b46afb..160c261d 100644 --- a/pylintrc +++ b/pylintrc @@ -6,8 +6,11 @@ # C0103 - Invalid name "%s" (should match %s) # C0111 - Missing docstring # E0102 - %s already defined line %s +# Does not understand @property getters and setters # E0202 - An attribute inherited from %s hide this method +# Does not understand @property getters and setters # E1101 - %s %r has no %r member +# Does not understand @property getters and setters # R0201 - Method could be a function # R0801 - Similar lines in %s files # R0903 - Too few public methods (%s/%s) From 32527bfe797ab808e22957de6547118e373a5c5f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 21:16:00 +0200 Subject: [PATCH 010/235] Merge branch 'develop', remote branch 'jodal/develop' into develop From c7c3020453e095725fb02752cefd31343755ddf2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 22:07:16 +0200 Subject: [PATCH 011/235] Add basic file scanner --- mopidy/scanner.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 mopidy/scanner.py diff --git a/mopidy/scanner.py b/mopidy/scanner.py new file mode 100644 index 00000000..385c0a1f --- /dev/null +++ b/mopidy/scanner.py @@ -0,0 +1,28 @@ +import gobject +gobject.threads_init() + +import pygst +pygst.require('0.10') +import gst +import sys + +def main(uri): + pipeline = gst.element_factory_make('playbin2') + + bus = pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message::tag', process_gst_message) + + pipeline.set_property('uri', uri) + pipeline.set_state(gst.STATE_PAUSED) + + gobject.MainLoop().run() + +def process_gst_message(bus, message): + data = message.parse_tag() + tags = dict([(k, data[k]) for k in data.keys()]) + + print tags + +if __name__ == '__main__': + main(sys.argv[1]) From 32d9316f09c369fc7ff4b777a2f052dbbd6f4a3e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 25 Oct 2010 22:07:28 +0200 Subject: [PATCH 012/235] Turn on warnings for use of the names of builtin functions and TODO/FIXME/XXX --- pylintrc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pylintrc b/pylintrc index 160c261d..dc7a9082 100644 --- a/pylintrc +++ b/pylintrc @@ -15,10 +15,8 @@ # R0801 - Similar lines in %s files # R0903 - Too few public methods (%s/%s) # R0904 - Too many public methods (%s/%s) -# W0141 - Used builtin function %r # R0921 - Abstract class not referenced # W0142 - Used * or ** magic -# W0511 - TODO, FIXME and XXX in the code # W0613 - Unused argument %r # -disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0511,W0613 +disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0142,W0613 From 2bc0c6bee0ad6a736f731a6b1b9bcfa7d22092c3 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 21:16:00 +0200 Subject: [PATCH 013/235] Merge branch 'develop', remote branch 'jodal/develop' into develop From 62ae85d05a4751c89276faa29816beafe7953ce3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 25 Oct 2010 22:14:32 +0200 Subject: [PATCH 014/235] Misinterpreted this warning. Disabling it again --- pylintrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylintrc b/pylintrc index dc7a9082..d2f84b77 100644 --- a/pylintrc +++ b/pylintrc @@ -16,7 +16,8 @@ # R0903 - Too few public methods (%s/%s) # R0904 - Too many public methods (%s/%s) # R0921 - Abstract class not referenced +# W0141 - Used builtin function '%s' # W0142 - Used * or ** magic # W0613 - Unused argument %r # -disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0142,W0613 +disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0613 From 53972f4022e4854f859860455902ffd7b658b5a7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 22:56:28 +0200 Subject: [PATCH 015/235] Rewrite to Scanner class --- mopidy/scanner.py | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 385c0a1f..4b42ed53 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -4,25 +4,46 @@ gobject.threads_init() import pygst pygst.require('0.10') import gst + +from os.path import abspath import sys +import threading -def main(uri): - pipeline = gst.element_factory_make('playbin2') +from mopidy.utils.path import path_to_uri - bus = pipeline.get_bus() - bus.add_signal_watch() - bus.connect('message::tag', process_gst_message) +class Scanner(object): + def __init__(self, files): + self.uris = [path_to_uri(abspath(f)) for f in files] - pipeline.set_property('uri', uri) - pipeline.set_state(gst.STATE_PAUSED) + self.pipe = gst.element_factory_make('playbin2') - gobject.MainLoop().run() + bus = self.pipe.get_bus() + bus.add_signal_watch() + bus.connect('message::tag', self.process_message) -def process_gst_message(bus, message): - data = message.parse_tag() - tags = dict([(k, data[k]) for k in data.keys()]) + self.next_uri() + + gobject.MainLoop().run() + + def process_message(self, bus, message): + data = message.parse_tag() + tags = dict([(k, data[k]) for k in data.keys()]) + + print tags + + self.next_uri() + + def next_uri(self): + if not self.uris: + sys.exit(0) + + self.pipe.set_state(gst.STATE_NULL) + self.pipe.set_property('uri', self.uris.pop()) + self.pipe.set_state(gst.STATE_PAUSED) - print tags if __name__ == '__main__': - main(sys.argv[1]) + if len(sys.argv) == 1: + sys.exit(1) + + Scanner(sys.argv[1:]) From 89fe08a583a5944bdff61195c82f93fc015c2084 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 23:02:09 +0200 Subject: [PATCH 016/235] Accept callback for actual data handling --- mopidy/scanner.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 4b42ed53..921d9272 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -12,8 +12,9 @@ import threading from mopidy.utils.path import path_to_uri class Scanner(object): - def __init__(self, files): + def __init__(self, files, callback): self.uris = [path_to_uri(abspath(f)) for f in files] + self.callback = callback self.pipe = gst.element_factory_make('playbin2') @@ -27,10 +28,7 @@ class Scanner(object): def process_message(self, bus, message): data = message.parse_tag() - tags = dict([(k, data[k]) for k in data.keys()]) - - print tags - + self.callback(dict([(k, data[k]) for k in data.keys()])) self.next_uri() def next_uri(self): @@ -41,9 +39,11 @@ class Scanner(object): self.pipe.set_property('uri', self.uris.pop()) self.pipe.set_state(gst.STATE_PAUSED) +def debug(data): + print data if __name__ == '__main__': if len(sys.argv) == 1: sys.exit(1) - Scanner(sys.argv[1:]) + Scanner(sys.argv[1:], debug) From 21123e3cd9bb2af2004a35c158b15826c0de1d2d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 23:18:35 +0200 Subject: [PATCH 017/235] Allow scanner to stop nicely when it runs out of files --- mopidy/scanner.py | 16 ++++++---------- tests/data/blank.mp3 | Bin 8208 -> 9360 bytes 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 921d9272..aa562ff1 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -15,6 +15,7 @@ class Scanner(object): def __init__(self, files, callback): self.uris = [path_to_uri(abspath(f)) for f in files] self.callback = callback + self.loop = gobject.MainLoop() self.pipe = gst.element_factory_make('playbin2') @@ -24,8 +25,6 @@ class Scanner(object): self.next_uri() - gobject.MainLoop().run() - def process_message(self, bus, message): data = message.parse_tag() self.callback(dict([(k, data[k]) for k in data.keys()])) @@ -33,17 +32,14 @@ class Scanner(object): def next_uri(self): if not self.uris: - sys.exit(0) + return self.stop() self.pipe.set_state(gst.STATE_NULL) self.pipe.set_property('uri', self.uris.pop()) self.pipe.set_state(gst.STATE_PAUSED) -def debug(data): - print data + def start(self): + self.loop.run() -if __name__ == '__main__': - if len(sys.argv) == 1: - sys.exit(1) - - Scanner(sys.argv[1:], debug) + def stop(self): + self.loop.quit() diff --git a/tests/data/blank.mp3 b/tests/data/blank.mp3 index 6aa48cd832849e6d598c48d7f11b9d84f3fc5983..ef159a700449f6a2bf4c03fc206be8f2ff1c7469 100644 GIT binary patch delta 258 zcmbQ>Fu^m*)5VyD0SMU3LOer^fDASU2Ii8?lAP3#09Qkh1dyLtRFYX-65{CN1XYlj zlT?};;u7QxIl*BQi+} Date: Mon, 25 Oct 2010 23:31:40 +0200 Subject: [PATCH 018/235] Add helper for finding files in folder --- mopidy/utils/path.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 0dd163ec..e73258ea 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -26,3 +26,9 @@ def path_to_uri(*paths): if sys.platform == 'win32': return 'file:' + urllib.pathname2url(path) return 'file://' + urllib.pathname2url(path) + +def find_files(folder): + for dirpath, dirnames, filenames in os.walk(folder): + for filename in filenames: + dirpath = os.path.abspath(dirpath) + yield os.path.join(dirpath, filename) From 14ecce6c6b27bbb8b523efdb3ec3b43ece5be7ce Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 23:31:55 +0200 Subject: [PATCH 019/235] Scanner takes folder instead of files --- mopidy/scanner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index aa562ff1..7bb088c9 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -9,11 +9,11 @@ from os.path import abspath import sys import threading -from mopidy.utils.path import path_to_uri +from mopidy.utils.path import path_to_uri, find_files class Scanner(object): - def __init__(self, files, callback): - self.uris = [path_to_uri(abspath(f)) for f in files] + def __init__(self, folder, callback): + self.uris = [path_to_uri(f) for f in find_files(folder)] self.callback = callback self.loop = gobject.MainLoop() From 10cf68a103daa7333d05ebb495088ff5b64ea71c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 23:38:26 +0200 Subject: [PATCH 020/235] Add basic detection of errors in scanner --- mopidy/scanner.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 7bb088c9..6b2798ef 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -21,15 +21,19 @@ class Scanner(object): bus = self.pipe.get_bus() bus.add_signal_watch() - bus.connect('message::tag', self.process_message) + bus.connect('message::tag', self.process_tags) + bus.connect('message::error', self.process_error) self.next_uri() - def process_message(self, bus, message): + def process_tags(self, bus, message): data = message.parse_tag() self.callback(dict([(k, data[k]) for k in data.keys()])) self.next_uri() + def process_error(self, bus, message): + print message.parse_error() + def next_uri(self): if not self.uris: return self.stop() From e8a1d1b49da80105dfff84ebfd71320f500a87ed Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 23:47:12 +0200 Subject: [PATCH 021/235] Add error callback and switch to using uridecodebin --- mopidy/scanner.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 6b2798ef..298d81d8 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -12,12 +12,15 @@ import threading from mopidy.utils.path import path_to_uri, find_files class Scanner(object): - def __init__(self, folder, callback): + def __init__(self, folder, data_callback, error_callback=None): self.uris = [path_to_uri(f) for f in find_files(folder)] - self.callback = callback + self.data_callback = data_callback + self.error_callback = error_callback self.loop = gobject.MainLoop() - self.pipe = gst.element_factory_make('playbin2') + self.uribin = gst.element_factory_make('uridecodebin') + self.pipe = gst.element_factory_make('pipeline') + self.pipe.add(self.uribin) bus = self.pipe.get_bus() bus.add_signal_watch() @@ -28,18 +31,22 @@ class Scanner(object): def process_tags(self, bus, message): data = message.parse_tag() - self.callback(dict([(k, data[k]) for k in data.keys()])) + self.data_callback(dict([(k, data[k]) for k in data.keys()])) self.next_uri() def process_error(self, bus, message): - print message.parse_error() + if self.error_callback: + uri = self.uribin.get_property('uri') + error = message.parse_error() + self.error_callback(uri, *error) + self.next_uri() def next_uri(self): if not self.uris: return self.stop() self.pipe.set_state(gst.STATE_NULL) - self.pipe.set_property('uri', self.uris.pop()) + self.uribin.set_property('uri', self.uris.pop()) self.pipe.set_state(gst.STATE_PAUSED) def start(self): From 7fdc01b99eba20aee56631d663ea9e059457c284 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Oct 2010 23:50:01 +0200 Subject: [PATCH 022/235] Move initial next_uri call to start method --- mopidy/scanner.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 298d81d8..e8c0c6ba 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -27,8 +27,6 @@ class Scanner(object): bus.connect('message::tag', self.process_tags) bus.connect('message::error', self.process_error) - self.next_uri() - def process_tags(self, bus, message): data = message.parse_tag() self.data_callback(dict([(k, data[k]) for k in data.keys()])) @@ -50,6 +48,7 @@ class Scanner(object): self.pipe.set_state(gst.STATE_PAUSED) def start(self): + self.next_uri() self.loop.run() def stop(self): From efc60a943b2c2e14707c46ab58f5eca2168a34ca Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 00:08:51 +0200 Subject: [PATCH 023/235] Update find files behaviour and add test for it --- mopidy/utils/path.py | 13 ++++++++----- tests/utils/path_test.py | 20 ++++++++++++++++++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index e73258ea..9220c53d 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -27,8 +27,11 @@ def path_to_uri(*paths): return 'file:' + urllib.pathname2url(path) return 'file://' + urllib.pathname2url(path) -def find_files(folder): - for dirpath, dirnames, filenames in os.walk(folder): - for filename in filenames: - dirpath = os.path.abspath(dirpath) - yield os.path.join(dirpath, filename) +def find_files(path): + if os.path.isfile(path): + yield os.path.abspath(path) + else: + for dirpath, dirnames, filenames in os.walk(path): + for filename in filenames: + dirpath = os.path.abspath(dirpath) + yield os.path.join(dirpath, filename) diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index ae63d5c0..e0359a16 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -6,9 +6,9 @@ import sys import tempfile import unittest -from mopidy.utils.path import get_or_create_folder, path_to_uri +from mopidy.utils.path import get_or_create_folder, path_to_uri, find_files -from tests import SkipTest +from tests import SkipTest, data_folder class GetOrCreateFolderTest(unittest.TestCase): def setUp(self): @@ -69,3 +69,19 @@ class PathToFileURITest(unittest.TestCase): else: result = path_to_uri(u'/tmp/æøå') self.assertEqual(result, u'file:///tmp/%C3%A6%C3%B8%C3%A5') + + +class FindFilesTest(unittest.TestCase): + def find(self, path): + return list(find_files(data_folder(path))) + + def test_basic_folder(self): + self.assert_(self.find('')) + + def test_nonexistant_folder(self): + self.assertEqual(self.find('does-not-exist'), []) + + def test_file(self): + files = self.find('blank.mp3') + self.assertEqual(len(files), 1) + self.assert_(files[0], data_folder('blank.mp3')) From 02bfad2fe48104e23e8ef60ea51e642dbdc8cf2c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 00:13:50 +0200 Subject: [PATCH 024/235] Add basic tests for scanner --- mopidy/scanner.py | 7 ++++--- tests/scanner.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 tests/scanner.py diff --git a/mopidy/scanner.py b/mopidy/scanner.py index e8c0c6ba..914f431e 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -29,14 +29,15 @@ class Scanner(object): def process_tags(self, bus, message): data = message.parse_tag() - self.data_callback(dict([(k, data[k]) for k in data.keys()])) + uri = self.uribin.get_property('uri') + self.data_callback(uri, dict([(k, data[k]) for k in data.keys()])) self.next_uri() def process_error(self, bus, message): if self.error_callback: uri = self.uribin.get_property('uri') - error = message.parse_error() - self.error_callback(uri, *error) + errors = message.parse_error() + self.error_callback(uri, errors) self.next_uri() def next_uri(self): diff --git a/tests/scanner.py b/tests/scanner.py new file mode 100644 index 00000000..5374e856 --- /dev/null +++ b/tests/scanner.py @@ -0,0 +1,45 @@ +import unittest + +from mopidy.scanner import Scanner + +from tests import data_folder + +class ScannerTest(unittest.TestCase): + def setUp(self): + self.errors = {} + self.data = {} + + def scan(self, path): + scanner = Scanner(data_folder(path), + self.data_callback, self.error_callback) + scanner.start() + + def data_callback(self, uri, data): + uri = uri.lstrip('file://') + uri = uri.lstrip(data_folder('')) + self.data[uri] = data + + def error_callback(self, uri, errors): + uri = uri.lstrip('file://') + uri = uri.lstrip(data_folder('')) + self.errors[uri] = errors + + def test_data_is_set(self): + self.scan('blank.mp3') + self.assert_(self.data) + + def test_errors_is_not_set(self): + self.scan('blank.mp3') + self.assert_(not self.errors) + + def test_artist_is_set(self): + self.scan('blank.mp3') + self.assertEqual(self.data['blank.mp3']['artist'], 'artist') + + def test_album_is_set(self): + self.scan('blank.mp3') + self.assertEqual(self.data['blank.mp3']['album'], 'album') + + def test_track_is_set(self): + self.scan('blank.mp3') + self.assertEqual(self.data['blank.mp3']['title'], 'title') From 704b5517e12e4b48321944ebabd6331d8c012ff4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 26 Oct 2010 13:25:52 +0200 Subject: [PATCH 025/235] Split xPlaybackController into xPlaybackController and xPlaybackProvider --- mopidy/backends/base/__init__.py | 3 +- mopidy/backends/base/playback.py | 126 ++++++++++++++----------- mopidy/backends/dummy/__init__.py | 53 ++++++----- mopidy/backends/libspotify/__init__.py | 14 ++- mopidy/backends/libspotify/playback.py | 18 ++-- mopidy/backends/local/__init__.py | 67 +++++++------ tests/backends/base/playback.py | 16 ++-- 7 files changed, 165 insertions(+), 132 deletions(-) diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index 491c5b73..f42a042c 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -6,7 +6,8 @@ import time from mopidy import settings from mopidy.backends.base.current_playlist import BaseCurrentPlaylistController from mopidy.backends.base.library import BaseLibraryController -from mopidy.backends.base.playback import BasePlaybackController +from mopidy.backends.base.playback import (BasePlaybackController, + BasePlaybackProvider) from mopidy.backends.base.stored_playlists import BaseStoredPlaylistsController from mopidy.frontends.mpd import translator from mopidy.models import Playlist diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index b7ceeee2..0d4ef52f 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -6,8 +6,10 @@ logger = logging.getLogger('mopidy.backends.base') class BasePlaybackController(object): """ - :param backend: backend the controller is a part of + :param backend: the backend :type backend: :class:`BaseBackend` + :param provider: provider the controller should use + :type provider: instance of :class:`BasePlaybackProvider` """ # pylint: disable = R0902 @@ -54,8 +56,9 @@ class BasePlaybackController(object): #: Playback continues after current song. single = False - def __init__(self, backend): + def __init__(self, backend, provider): self.backend = backend + self.provider = provider self._state = self.STOPPED self._shuffled = [] self._first_shuffle = True @@ -353,18 +356,9 @@ class BasePlaybackController(object): def pause(self): """Pause playback.""" - if self.state == self.PLAYING and self._pause(): + if self.state == self.PLAYING and self.provider.pause(): self.state = self.PAUSED - def _pause(self): - """ - To be overridden by subclass. Implement your backend's pause - functionality here. - - :rtype: :class:`True` if successful, else :class:`False` - """ - raise NotImplementedError - def play(self, cp_track=None, on_error_step=1): """ Play the given track, or if the given track is :class:`None`, play the @@ -391,7 +385,7 @@ class BasePlaybackController(object): self.state = self.STOPPED self.current_cp_track = cp_track self.state = self.PLAYING - if not self._play(cp_track[1]): + if not self.provider.play(cp_track[1]): # Track is not playable if self.random and self._shuffled: self._shuffled.remove(cp_track) @@ -405,18 +399,6 @@ class BasePlaybackController(object): self._trigger_started_playing_event() - def _play(self, track): - """ - To be overridden by subclass. Implement your backend's play - functionality here. - - :param track: the track to play - :type track: :class:`mopidy.models.Track` - :rtype: :class:`True` if successful, else :class:`False` - """ - - raise NotImplementedError - def previous(self): """Play the previous track.""" if self.cp_track_at_previous is None: @@ -428,18 +410,9 @@ class BasePlaybackController(object): def resume(self): """If paused, resume playing the current track.""" - if self.state == self.PAUSED and self._resume(): + if self.state == self.PAUSED and self.provider.resume(): self.state = self.PLAYING - def _resume(self): - """ - To be overridden by subclass. Implement your backend's resume - functionality here. - - :rtype: :class:`True` if successful, else :class:`False` - """ - raise NotImplementedError - def seek(self, time_position): """ Seeks to time position given in milliseconds. @@ -465,18 +438,7 @@ class BasePlaybackController(object): self._play_time_started = self._current_wall_time self._play_time_accumulated = time_position - return self._seek(time_position) - - def _seek(self, time_position): - """ - To be overridden by subclass. Implement your backend's seek - functionality here. - - :param time_position: time position in milliseconds - :type time_position: int - :rtype: :class:`True` if successful, else :class:`False` - """ - raise NotImplementedError + return self.provider.seek(time_position) def stop(self, clear_current_track=False): """ @@ -489,20 +451,11 @@ class BasePlaybackController(object): if self.state == self.STOPPED: return self._trigger_stopped_playing_event() - if self._stop(): + if self.provider.stop(): self.state = self.STOPPED if clear_current_track: self.current_cp_track = None - def _stop(self): - """ - To be overridden by subclass. Implement your backend's stop - functionality here. - - :rtype: :class:`True` if successful, else :class:`False` - """ - raise NotImplementedError - def _trigger_started_playing_event(self): """ Notifies frontends that a track has started playing. @@ -532,3 +485,62 @@ class BasePlaybackController(object): 'track': self.current_track, 'stop_position': self.time_position, }) + + +class BasePlaybackProvider(object): + """ + :param backend: the backend + :type backend: :class:`BaseBackend` + """ + + def __init__(self, backend): + self.backend = backend + + def pause(self): + """ + To be overridden by subclass. Implement your backend's pause + functionality here. + + :rtype: :class:`True` if successful, else :class:`False` + """ + raise NotImplementedError + + def play(self, track): + """ + To be overridden by subclass. Implement your backend's play + functionality here. + + :param track: the track to play + :type track: :class:`mopidy.models.Track` + :rtype: :class:`True` if successful, else :class:`False` + """ + raise NotImplementedError + + def resume(self): + """ + To be overridden by subclass. Implement your backend's resume + functionality here. + + :rtype: :class:`True` if successful, else :class:`False` + """ + raise NotImplementedError + + def seek(self, time_position): + """ + To be overridden by subclass. Implement your backend's seek + functionality here. + + :param time_position: time position in milliseconds + :type time_position: int + :rtype: :class:`True` if successful, else :class:`False` + """ + raise NotImplementedError + + def stop(self): + """ + To be overridden by subclass. Implement your backend's stop + functionality here. + + :rtype: :class:`True` if successful, else :class:`False` + """ + raise NotImplementedError diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 62cbd7e2..7a2788b7 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -1,5 +1,5 @@ from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, - BasePlaybackController, BaseLibraryController, + BasePlaybackController, BasePlaybackProvider, BaseLibraryController, BaseStoredPlaylistsController) from mopidy.models import Playlist @@ -13,10 +13,17 @@ class DummyBackend(BaseBackend): def __init__(self, *args, **kwargs): super(DummyBackend, self).__init__(*args, **kwargs) + self.current_playlist = DummyCurrentPlaylistController(backend=self) + self.library = DummyLibraryController(backend=self) - self.playback = DummyPlaybackController(backend=self) + + playback_provider = DummyPlaybackProvider(backend=self) + self.playback = DummyPlaybackController(backend=self, + provider=playback_provider) + self.stored_playlists = DummyStoredPlaylistsController(backend=self) + self.uri_handlers = [u'dummy:'] @@ -43,30 +50,6 @@ class DummyLibraryController(BaseLibraryController): class DummyPlaybackController(BasePlaybackController): - def _next(self, track): - """Pass None as track to force failure""" - return track is not None - - def _pause(self): - return True - - def _play(self, track): - """Pass None as track to force failure""" - return track is not None - - def _previous(self, track): - """Pass None as track to force failure""" - return track is not None - - def _resume(self): - return True - - def _seek(self, time_position): - return True - - def _stop(self): - return True - def _trigger_started_playing_event(self): pass # noop @@ -74,6 +57,24 @@ class DummyPlaybackController(BasePlaybackController): pass # noop +class DummyPlaybackProvider(BasePlaybackProvider): + def pause(self): + return True + + def play(self, track): + """Pass None as track to force failure""" + return track is not None + + def resume(self): + return True + + def seek(self, time_position): + return True + + def stop(self): + return True + + class DummyStoredPlaylistsController(BaseStoredPlaylistsController): _playlists = [] diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index 223d9968..c067cb67 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -1,7 +1,8 @@ import logging from mopidy import settings -from mopidy.backends.base import BaseBackend, BaseCurrentPlaylistController +from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, + BasePlaybackController) logger = logging.getLogger('mopidy.backends.libspotify') @@ -34,17 +35,24 @@ class LibspotifyBackend(BaseBackend): def __init__(self, *args, **kwargs): from .library import LibspotifyLibraryController - from .playback import LibspotifyPlaybackController + from .playback import LibspotifyPlaybackProvider from .stored_playlists import LibspotifyStoredPlaylistsController super(LibspotifyBackend, self).__init__(*args, **kwargs) self.current_playlist = BaseCurrentPlaylistController(backend=self) + self.library = LibspotifyLibraryController(backend=self) - self.playback = LibspotifyPlaybackController(backend=self) + + playback_provider = LibspotifyPlaybackProvider(backend=self) + self.playback = BasePlaybackController(backend=self, + provider=playback_provider) + self.stored_playlists = LibspotifyStoredPlaylistsController( backend=self) + self.uri_handlers = [u'spotify:', u'http://open.spotify.com/'] + self.spotify = self._connect() def _connect(self): diff --git a/mopidy/backends/libspotify/playback.py b/mopidy/backends/libspotify/playback.py index 39c56bf6..29409ff4 100644 --- a/mopidy/backends/libspotify/playback.py +++ b/mopidy/backends/libspotify/playback.py @@ -2,17 +2,17 @@ import logging from spotify import Link, SpotifyError -from mopidy.backends.base import BasePlaybackController +from mopidy.backends.base import BasePlaybackProvider logger = logging.getLogger('mopidy.backends.libspotify.playback') -class LibspotifyPlaybackController(BasePlaybackController): - def _pause(self): +class LibspotifyPlaybackProvider(BasePlaybackProvider): + def pause(self): return self.backend.output.set_state('PAUSED') - def _play(self, track): + def play(self, track): self.backend.output.set_state('READY') - if self.state == self.PLAYING: + if self.backend.playback.state == self.backend.playback.PLAYING: self.backend.spotify.session.play(0) if track.uri is None: return False @@ -26,16 +26,16 @@ class LibspotifyPlaybackController(BasePlaybackController): logger.warning('Play %s failed: %s', track.uri, e) return False - def _resume(self): - return self._seek(self.time_position) + def resume(self): + return self.seek(self.backend.playback.time_position) - def _seek(self, time_position): + def seek(self, time_position): self.backend.output.set_state('READY') self.backend.spotify.session.seek(time_position) self.backend.output.set_state('PLAYING') return True - def _stop(self): + def stop(self): result = self.backend.output.set_state('READY') self.backend.spotify.session.play(0) return result diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 04761e17..4ad8947b 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -5,9 +5,10 @@ import os import shutil from mopidy import settings -from mopidy.backends.base import (BaseBackend, BaseLibraryController, - BaseStoredPlaylistsController, BaseCurrentPlaylistController, - BasePlaybackController) +from mopidy.backends.base import (BaseBackend, + BaseCurrentPlaylistController, BaseLibraryController, + BasePlaybackController, BasePlaybackProvider, + BaseStoredPlaylistsController) from mopidy.models import Playlist, Track, Album from mopidy.utils.process import pickle_connection @@ -31,41 +32,51 @@ class LocalBackend(BaseBackend): def __init__(self, *args, **kwargs): super(LocalBackend, self).__init__(*args, **kwargs) - self.library = LocalLibraryController(self) - self.stored_playlists = LocalStoredPlaylistsController(self) - self.current_playlist = BaseCurrentPlaylistController(self) - self.playback = LocalPlaybackController(self) + self.library = LocalLibraryController(backend=self) + + self.stored_playlists = LocalStoredPlaylistsController(backend=self) + + self.current_playlist = BaseCurrentPlaylistController(backend=self) + + playback_provider = LocalPlaybackProvider(backend=self) + self.playback = LocalPlaybackController(backend=self, + provider=playback_provider) + self.uri_handlers = [u'file://'] class LocalPlaybackController(BasePlaybackController): - def __init__(self, backend): - super(LocalPlaybackController, self).__init__(backend) + def __init__(self, *args, **kwargs): + super(LocalPlaybackController, self).__init__(*args, **kwargs) + + # XXX Why do we call stop()? Is it to set GStreamer state to 'READY'? self.stop() - def _play(self, track): - return self.backend.output.play_uri(track.uri) - - def _stop(self): - return self.backend.output.set_state('READY') - - def _pause(self): - return self.backend.output.set_state('PAUSED') - - def _resume(self): - return self.backend.output.set_state('PLAYING') - - def _seek(self, time_position): - return self.backend.output.set_position(time_position) - @property def time_position(self): return self.backend.output.get_position() +class LocalPlaybackProvider(BasePlaybackProvider): + def pause(self): + return self.backend.output.set_state('PAUSED') + + def play(self, track): + return self.backend.output.play_uri(track.uri) + + def resume(self): + return self.backend.output.set_state('PLAYING') + + def seek(self, time_position): + return self.backend.output.set_position(time_position) + + def stop(self): + return self.backend.output.set_state('READY') + + class LocalStoredPlaylistsController(BaseStoredPlaylistsController): - def __init__(self, *args): - super(LocalStoredPlaylistsController, self).__init__(*args) + def __init__(self, *args, **kwargs): + super(LocalStoredPlaylistsController, self).__init__(*args, **kwargs) self._folder = os.path.expanduser(settings.LOCAL_PLAYLIST_FOLDER) self.refresh() @@ -137,8 +148,8 @@ class LocalStoredPlaylistsController(BaseStoredPlaylistsController): class LocalLibraryController(BaseLibraryController): - def __init__(self, backend): - super(LocalLibraryController, self).__init__(backend) + def __init__(self, *args, **kwargs): + super(LocalLibraryController, self).__init__(*args, **kwargs) self._uri_mapping = {} self.refresh() diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index 4caaf44b..7b6efe7a 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -104,8 +104,8 @@ class BasePlaybackControllerTest(object): @populate_playlist def test_play_skips_to_next_track_on_failure(self): - # If _play() returns False, it is a failure. - self.playback._play = lambda track: track != self.tracks[0] + # If provider.play() returns False, it is a failure. + self.playback.provider.play = lambda track: track != self.tracks[0] self.playback.play() self.assertNotEqual(self.playback.current_track, self.tracks[0]) self.assertEqual(self.playback.current_track, self.tracks[1]) @@ -164,8 +164,8 @@ class BasePlaybackControllerTest(object): @populate_playlist def test_previous_skips_to_previous_track_on_failure(self): - # If _play() returns False, it is a failure. - self.playback._play = lambda track: track != self.tracks[1] + # If provider.play() returns False, it is a failure. + self.playback.provider.play = lambda track: track != self.tracks[1] self.playback.play(self.current_playlist.cp_tracks[2]) self.assertEqual(self.playback.current_track, self.tracks[2]) self.playback.previous() @@ -228,8 +228,8 @@ class BasePlaybackControllerTest(object): @populate_playlist def test_next_skips_to_next_track_on_failure(self): - # If _play() returns False, it is a failure. - self.playback._play = lambda track: track != self.tracks[1] + # If provider.play() returns False, it is a failure. + self.playback.provider.play = lambda track: track != self.tracks[1] self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[0]) self.playback.next() @@ -364,8 +364,8 @@ class BasePlaybackControllerTest(object): @populate_playlist def test_end_of_track_skips_to_next_track_on_failure(self): - # If _play() returns False, it is a failure. - self.playback._play = lambda track: track != self.tracks[1] + # If provider.play() returns False, it is a failure. + self.playback.provider.play = lambda track: track != self.tracks[1] self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[0]) self.playback.on_end_of_track() From 726b62887cb494b52802d31797acc6a23f3ca9cc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 26 Oct 2010 13:59:27 +0200 Subject: [PATCH 026/235] Add BasePlaybackProvider to __all__ --- mopidy/backends/base/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index f42a042c..eb75467c 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -15,7 +15,7 @@ from mopidy.utils import get_class logger = logging.getLogger('mopidy.backends.base') -__all__ = ['BaseBackend', 'BasePlaybackController', +__all__ = ['BaseBackend', 'BasePlaybackController', 'BasePlaybackProvider', 'BaseCurrentPlaylistController', 'BaseStoredPlaylistsController', 'BaseLibraryController'] From 23881e1b1d28274d7f0c1c5a63d1659c6944ff19 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 21:33:12 +0200 Subject: [PATCH 027/235] Add basic test structure for scanner testing --- tests/data/scanner/advanced/song1.mp3 | 1 + tests/data/scanner/advanced/song2.mp3 | 1 + tests/data/scanner/advanced/song3.mp3 | 1 + tests/data/scanner/advanced/subdir1/song4.mp3 | 1 + tests/data/scanner/advanced/subdir1/song5.mp3 | 1 + .../scanner/advanced/subdir1/subsubdir/song8.mp3 | 1 + .../scanner/advanced/subdir1/subsubdir/song9.mp3 | 1 + tests/data/scanner/advanced/subdir2/song6.mp3 | 1 + tests/data/scanner/advanced/subdir2/song7.mp3 | 1 + tests/data/scanner/empty/.gitignore | 0 tests/data/scanner/sample.mp3 | Bin 0 -> 9360 bytes tests/data/scanner/simple/song1.mp3 | 1 + 12 files changed, 10 insertions(+) create mode 120000 tests/data/scanner/advanced/song1.mp3 create mode 120000 tests/data/scanner/advanced/song2.mp3 create mode 120000 tests/data/scanner/advanced/song3.mp3 create mode 120000 tests/data/scanner/advanced/subdir1/song4.mp3 create mode 120000 tests/data/scanner/advanced/subdir1/song5.mp3 create mode 120000 tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 create mode 120000 tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 create mode 120000 tests/data/scanner/advanced/subdir2/song6.mp3 create mode 120000 tests/data/scanner/advanced/subdir2/song7.mp3 create mode 100644 tests/data/scanner/empty/.gitignore create mode 100644 tests/data/scanner/sample.mp3 create mode 120000 tests/data/scanner/simple/song1.mp3 diff --git a/tests/data/scanner/advanced/song1.mp3 b/tests/data/scanner/advanced/song1.mp3 new file mode 120000 index 00000000..6896a7a2 --- /dev/null +++ b/tests/data/scanner/advanced/song1.mp3 @@ -0,0 +1 @@ +../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/song2.mp3 b/tests/data/scanner/advanced/song2.mp3 new file mode 120000 index 00000000..6896a7a2 --- /dev/null +++ b/tests/data/scanner/advanced/song2.mp3 @@ -0,0 +1 @@ +../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/song3.mp3 b/tests/data/scanner/advanced/song3.mp3 new file mode 120000 index 00000000..6896a7a2 --- /dev/null +++ b/tests/data/scanner/advanced/song3.mp3 @@ -0,0 +1 @@ +../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir1/song4.mp3 b/tests/data/scanner/advanced/subdir1/song4.mp3 new file mode 120000 index 00000000..45812ac5 --- /dev/null +++ b/tests/data/scanner/advanced/subdir1/song4.mp3 @@ -0,0 +1 @@ +../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir1/song5.mp3 b/tests/data/scanner/advanced/subdir1/song5.mp3 new file mode 120000 index 00000000..45812ac5 --- /dev/null +++ b/tests/data/scanner/advanced/subdir1/song5.mp3 @@ -0,0 +1 @@ +../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 new file mode 120000 index 00000000..45812ac5 --- /dev/null +++ b/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 @@ -0,0 +1 @@ +../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 new file mode 120000 index 00000000..45812ac5 --- /dev/null +++ b/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 @@ -0,0 +1 @@ +../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir2/song6.mp3 b/tests/data/scanner/advanced/subdir2/song6.mp3 new file mode 120000 index 00000000..45812ac5 --- /dev/null +++ b/tests/data/scanner/advanced/subdir2/song6.mp3 @@ -0,0 +1 @@ +../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir2/song7.mp3 b/tests/data/scanner/advanced/subdir2/song7.mp3 new file mode 120000 index 00000000..45812ac5 --- /dev/null +++ b/tests/data/scanner/advanced/subdir2/song7.mp3 @@ -0,0 +1 @@ +../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/empty/.gitignore b/tests/data/scanner/empty/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/scanner/sample.mp3 b/tests/data/scanner/sample.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ad5aa37a97b42325de1381c519052cf17693e7dc GIT binary patch literal 9360 zcmeHtS5#B&)^6xU5Ru+OkrH~9t`ZOk5RewS3P`VlbX2N9Kza|ogc5p(03wKVr6XNY zL_xt15u|*Oe=YYu``qmD@5^&8_B#e+uu9f?<}>?zm(FcjQV6gp0!(#GWq^OEArKNj zU%LnHUUr_2riPl*z&SGD+@C`@QvIK|*m>Cdd;U3h+gRhDx5!9I$(tH$=mCf1z%Qhv zC8fY8{&)SCf&Vh_|0x56Pl&;PL9=l6at03QfIm0{!gB>eOhrX|^(qS+8!v9^zJOXZMo?V=2opX+ztJ-T# zsir5tMJwO7{J0;qoKT}S=+@W$dw0FzXdcy;xI_4)I4Gtd7LePQjDR%Gk9f)%#tb;X zW+on4`>4&ZUQ;X1-RYv{mx!#QBV}?7u$GLD2tV$RNlCQ}DDPo3*B-xWpD>!0n$jOp z&JZcwKX9K*-a4%+U0a(Z zJR=CJw-7Rg-?qDYY6YS(_~hSzLx|sm%{SCOnV9sK_WhX>^dpGy2{y>iH?}v?3(%WxhfLJ&^$lhejPoPmh!ZdDabGZ-e(()v~-uj%{q_y;vs z+M16L+UB~-y}$P=QQx1Rh;KgFtKemt=0gh_mERS87aly~=O%Z<&-dns@Arws-eZuh1nB4?X*d*Dw?|bNgObEWA8dNBX3{Ls~bu zaI`-(p2^>}D{7Q_kyiC_jY=S!TspbaocoipPfrbA!Ug9f!Vd=8v1@(uTuy~ky<|YqqhPH5$jB@(7+sazmH{EEaSo~-;3js1(6Nx^_=YmI zosM;MMoVF;oMEhDv>+ZBXLZOhR1rzT|E$1C>o~#ettXuA$KKAmVEk=XCdw}R0U!Dl zxRwrst-h^;G*aW$Hbum><|&mSs&#BYe!%YvJbRHRVy(NX&1f9?lT|_|U0>HJ4wHv3 zYG;KIiSYe!UcGhWH*fpr4=euI9M&W))j7N7a7bo-I3)7tm$j>#Q6Gr{oKZG&^@k%H4!hA5@1{Kgj~gWh)A zCutmrM~GweXdG&+Wom0c7xw@`I>T%WRXtT9W)>_~>fsn3r_XFQ=JS<3NGNo3_lAH+Jm12vwg@hyf_w6<2Ca#nV962Bce6W?n3 z%&il;LAe-M?9?Zr$~0o~AF5oPIN3WXUaD3OpMO%|<$Hh-dUILaCFgOfv^=bJWXz18 z<0d-`ZS44^aCOb`)>kGkbS1AEHULrUbe_p89`81=R{bbw(yYxXD7U7o=&%z)^dj$U z;>X?nrUv~X&d0OZHyQRmJf_t#;@l(LF4PpnoV1;3P^qrjF8#IVzfSc3VLzi|fLzfw z#$T?f5Z1E-uh)xV(D}+@ViuR7P?%eL@=rE;2;y*h519x#K&mT?~V zQBUBUf^$K+NZ3-JPC^wY#A@(GdpQsymAlF=mPaDBwV|r72W9HoU+H5bggG{!(Kju? zwlUjja+@lZibKZ4wcp);bxEF%-8bay6-wD3Wz?9GVqlhMI%c22M(D1`?xby!cUQW@bEO&f??HXS#({ zCpjxutj=K>*)QU@VG`es2FLcIM_LOQ?NJH6F^6R+$g?E7(!wxQS<7#CoXHSxxs=9fe%<_u zjiRgvJ2&|T8{6!&yyP9mSkspSDbzuBl0uLcPRypt6--?ifiO_V?5yv(U{KL{h7^ce2v?@BRBf*7lV8>LjC6 z96Ar5e7Fc0_LqvI@0Diwce1s*RAk8`w&gvM2aa*?sX-Nu3VW16hV2qAfM6w>^4ONMDBtjr1EhtGJZT8oeGB@y-fZusJA^IPEO8H8wCfIm zaJ0HVB5aaguGr`^bhIWwp1C>u zLHn5z!}UqcdS-c%vt+Oj(6{-_B^U%U>Ev!bI6q!ny?G=zT~e$!DQ$x6MY0F^evtY~ zv)r+KP~TJH=e8;vRNvmTgYY7LQe!V#krrbd|$t*Aj)0Nx`2-&ewJ!)dl zWiZt){-VXyp5l|Wie)UredBR*wcOT?n5?+Q4m1!#h;k$IHb7AVEPT<{@wco#T4(-SM@A&2m&8o(oTrGwb zn*FKz+JIO0x%i=>Qo~6po>`CM-%ae})aItzwSA}%vuJY8tNz$hEqG-PnqN&Vwk=RN zzTy4-u&Cz@irhnLhM+y)X5S9^gGZ{^2Et22l*U-(j)_fa5a}p!ny;Dq#MRemvUK<& znub&2^~=2%1V3e|v%rH{TNW|3J*b(Ki?o$lI z+dQK^siV_W?^0EDj=AhBjR&N`&EhlX$`T7y&vqU5i7fr(EhFn( z=O%YdQY_z^1%U|?l#86B+85B*jMQE$gs3f~_1jsgmwOSzvcBRV)8*O42;yUGb2CZ# z1S8!Jsr4JqACIAPfK`^2oYXJM;;IfhK*GF72(IGV}xsVD2z ziq>U|ezSWL)y^^tVd8B!Phr*XePIrzW`M?V$+bW=tyD1GrYEp^I%mOmfY7y<%=RRlt=@*L|l4lBDWMOTCFhDH%wF)6p2IacM z*#yQ7V;a{JHPi5dMpBXRvJ_lA1A0MOB!1103@=YULW8Z)^>eSUZ%lW-W|Wpsx2?;t+Q+(0nU~P9RFYU&W+Za_S%-re=3?`~!D2b67v+Fkx)v|sMwS`3Rr`9I5_%2y&F>^yJ#{rZUo4uR!o?< zXm2hHR(z)6Ma79efi|wK${UTC+EKT*+2-7IA{Vp`Ro7##rW)Ey$m<&S>didwzWCDZ zIr+HRxYs77>F`njom!$fo81uSpR5F$IFV%Q&yIKeVLX`&)wv?`+|ds^tWsKL`Az9x zP19)UHy_+%?NT`g@km19Af5@@mJy~J{@e?v-YrMh2m!W$hGggd_ugH3Pir~6gnyr- zJd&0Yye5tMB)>BUt{9*ab8Cc^xcpx@X#es)?siN5eRjIMUfJw?E0^CqE*CdmbOCe| zzs_KC(bIHmQC>DSOC2We#;L1N%LBAcM}O(t+oYCPZcl0Ta+7-~Xf z`x zO$Ru7o8O3YH`%OL!xhFvoP%@o@kMp4K-nvl$F#v%KOgs&KCk6_M$a&>2QLjJh=+%b z4Xh7m7?%~%B$q@UZg83}@>CyX&z}#>J1;+iE4k5TjX##J7TDNius366vP^Xwe`%Ve zP-EWlOVn!GUoMPrwvH*BOOn02P>1@5qH6&o>x&+rO?N474QvR+>iX1~;Emlo8Pk*; zEfKkC=ywY)tV-!i*&*H)X+ISLslr)R7ghO!L7s1N&;xNyae`(}d@zB!D73D4plRfR zXz5=Jrb^Un=iR^Eq#e8KI6~8XHfjVv%+WY%YO%tZH0J}+XrQM#T7?h`V?eoRxK03` z4QCjKcVm*X{vw6{9CNTA;5#BVVTAG-nevIWo9FZL^Ol<#;%o8}C!Jwu<lyuu8 z9@>70c8gBpX}|r}%_$9GI>6~K>9m;KPM?r5PW6Gj!M4n>;El9%iEdoZm&-(IyFpo% zEX*WTMkp#{=b(7pSkWF(6RDi;%7+j-tCGe-qq0CeEF8Qcpx!@tJj#$}QPwhuo8glY zu2-h95O(u;{@@Enz^;>b;$f4`P^ z;N5g1CX8_WlW1%~)gnAslJh4mu?X1V05(>a10lM8CV9yfLu~{pc52|u+-aF0BFX7d zFD@XGP4oD8_nNQE2?K~n9GC`rdn99?NtJo50 z^6R`o>U^ao3%zU+GWbSJZH~U@PWg=A$y$)+1C17|7%Tr%F@YlTk#s)3n_HKCN&*!bO-KjYTSqqaHL>rt;(Ldx zLO*Kg>pcEtntr)m)ogv)52uCB!Q+d*13ixa?nJi&@n(f**U;UG5@OQLQXb-25)cm$ zE9hUa39lVOkz&-&C@11&)nohqrkFs|0UgUPAkXTomdc=i@e>>J<~m!ls;Bjx zhY>E`dEHxi+S>bsCO5Qjq3x?WiJMtq9IhrgU=BjmI)iG;P%6)iGn6)-Ej0_K2Ib=5 zva|=DgQVe;H1!{7TYawfnf;FHJvigNtMt6_aIyhgA8{4kcO-O7RMc_s>k)6o)Nu4= zi(-*>v9U=Mk3L4ix276>7XuR@&uskgBYBT)4oubgrJ`&D_I#_3qzYf1Mfy8~DYNLJ z2SS1?pzW3)wdn|{_-S0D@zVg6SHV5v$uQ3A%U46I0OEl`eSmp_G18F#p->v)qlVTU zt@ev_%1?e}eppwiGa9zjoE(A4ZZvk0Q~hAGWb&GggYJfyErL~s!lD!tJhWb|EsPCx z%s9WO@bDgs(8>4ZUX(JDbrBQOG5ga6fTeKzLCEqu{#RZJ-B&k_Q3`D=X*`yi;}K}n z=#dX`rdnPu?=)Jl$!;JXY0iic(7#~Yq7_sXv--Lphcl`V@tJcxo$_+jR1v05Q-eL} zqKG=?SD$ggLi~Irav|@}ynH0D^_dRqy@MRp`DWE0Z|uZh0U{d?-|(;#LVWG4K*HuP zGj7HxiOY{y9X6mlLfqG71(sxf7ck!7kv1)#QoLcWo!o`U+-*O6V~6*s7nfouHsJZA zcQzwn?q@hb+W3;ukl*q9k;g$>lhgVUt>=3J;r@Np4Rn)NiFb6!7?u6&OtZjPOjTIj zB7~?gq%z+1oenw&t*7Gz%EbchcNzxbSa~_Vyz2GHMf2G6MtNO>u36GOA*;B*=6Xfg zS;x|8D)0Q75La3Ca@A#e_&BB5Dw9Zy3QBKV>)^Y})f`nebtd{XICLJ)ZN{hkE;`DI zx{R1ep8wD=#MXW_x>17T3N!0NWdDR&P!Ld-g3Nb+@j`|-jRO5uqF=J4>9#Q}XeI~5 z!_B#b2Y5E5QBn;VxhPcGLxjIq8`@pqdh zDPs{{s#D>?aB;z9J1?d6CH^;ty)|~a_$0$*9{g*}#`h$+V?0j_DYbSmSTY;|9v1V6L-%t;&$*1r{INp`_I~^%5PxbpD`sscT(x&} z8!Pxr7D zw1Ej7FyRG)>XS{}-E>(Mvn4o_Q;n&=MP-RcfqRy$s~Fmhjxkq)qBtd!-39AuZPDUU z(v#P~{+Es%dn1iY_;?2F&%92HJ=hJ1KQmXPlkRQ8wum$(QElHt+y}^tPIz45c8Iq7 z3U(}_?!C2ZML{2d@Wj=&1)2QgiF}GZ;9@v@(P0iysmz2n3%q?)h=D#3YsHEuM;oqe z;jX{qOD_+`E5N@NGS5-+fp{c1?FPU)Jmn*KrtQL)X!-#eJ6#Oi)8M8WIWuD4`rwh6 z0G}QFUBqwcYHA8ChDHMOBXLztiL{ye=Dv?dp$aaNj7*6d9-(6Kcnq+S0MOmY3cTKJ zw@dZjBHv7anl(vA<|TX@g-J@-09~xkR9fT^&+V3(W{T&xjP5|~$!nWWB2pBCC~1Md ziFNQ63!4E@M@@)Hp(fPl6%#PnH+}krnQ1?C3E*04kv8#Ee>IN^3I;lQuoA{UEnpC` z*E(4K1!Z|8-OR$5BnryK#R>nDmyzuv`PFQ)hL|mEQz2aaO2F6|l#7VjC;*)Q7{&wbeZyY>0o~hgXF8>_pP}$bQ#P3~u+Sg#%_(qwCX2K@M5M2rmjN@II@A#%-z=jm zo1pHd2^cRf%o8XH&a)4M9j;!Prx)}Y=3tE~GjLKE&se{#pOeByqBafv(^racEXDuX zmtpYaH&BhRpzgOyUOnK3N-!KRiC&7E*)S=wwUv9DW$CEnb$yr}vrvH69Vw})3G%G5 z4xAg8uDed9j%A!3L5+@&8eo}#ac3UxPpPf2-DiF~xAONps|}%0T@->V`rAI!Mj3@B4DX~ z=lPI=5Rj?K`tkMWxsSfxoF?PB>0GX3S#@npku(zA_v9A!yNSOaug0T^tK4yOo7}$f z;GV1Cg>q-#*N}F@?dh$Uk*aBtkxzmW0_Nq_31x0ay$N8yI$^LOcEH zX}YFuYBVq{8V}@tq-P&%F>2BDC13dtJ<%+cqFp@?rtN82FGZ^HLagzZ@dV(DyTU@w-^3-5{OCv1E8N$kpKVy literal 0 HcmV?d00001 diff --git a/tests/data/scanner/simple/song1.mp3 b/tests/data/scanner/simple/song1.mp3 new file mode 120000 index 00000000..6896a7a2 --- /dev/null +++ b/tests/data/scanner/simple/song1.mp3 @@ -0,0 +1 @@ +../sample.mp3 \ No newline at end of file From 8b963311e1e1449fd34c1a68bbe859fe51cb9fdb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 21:38:46 +0200 Subject: [PATCH 028/235] Update scanner test --- tests/scanner.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/scanner.py b/tests/scanner.py index 5374e856..091e8846 100644 --- a/tests/scanner.py +++ b/tests/scanner.py @@ -14,32 +14,34 @@ class ScannerTest(unittest.TestCase): self.data_callback, self.error_callback) scanner.start() + def check(self, name, key, value): + name = data_folder(name) + self.assertEqual(self.data[name][key], value) + def data_callback(self, uri, data): - uri = uri.lstrip('file://') - uri = uri.lstrip(data_folder('')) + uri = uri[len('file://'):] self.data[uri] = data def error_callback(self, uri, errors): - uri = uri.lstrip('file://') - uri = uri.lstrip(data_folder('')) + uri = uri[len('file://'):] self.errors[uri] = errors def test_data_is_set(self): - self.scan('blank.mp3') + self.scan('scanner/simple') self.assert_(self.data) def test_errors_is_not_set(self): - self.scan('blank.mp3') + self.scan('scanner/simple') self.assert_(not self.errors) def test_artist_is_set(self): - self.scan('blank.mp3') - self.assertEqual(self.data['blank.mp3']['artist'], 'artist') + self.scan('scanner/simple') + self.check('scanner/simple/song1.mp3', 'artist', 'name') def test_album_is_set(self): - self.scan('blank.mp3') - self.assertEqual(self.data['blank.mp3']['album'], 'album') + self.scan('scanner/simple') + self.check('scanner/simple/song1.mp3', 'album', 'albumname') def test_track_is_set(self): - self.scan('blank.mp3') - self.assertEqual(self.data['blank.mp3']['title'], 'title') + self.scan('scanner/simple') + self.check('scanner/simple/song1.mp3', 'title', 'trackname') From 71e2f2c43a485199a3d650c60a8dfb05297aa739 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 21:51:22 +0200 Subject: [PATCH 029/235] Add mpd generated sample tag caches --- tests/data/scanner/advanced_cache | 81 +++++++++++++++++++++++++++++++ tests/data/scanner/empty_cache | 6 +++ tests/data/scanner/simple_cache | 15 ++++++ 3 files changed, 102 insertions(+) create mode 100644 tests/data/scanner/advanced_cache create mode 100644 tests/data/scanner/empty_cache create mode 100644 tests/data/scanner/simple_cache diff --git a/tests/data/scanner/advanced_cache b/tests/data/scanner/advanced_cache new file mode 100644 index 00000000..60f7fca6 --- /dev/null +++ b/tests/data/scanner/advanced_cache @@ -0,0 +1,81 @@ +info_begin +mpd_version: 0.15.4 +fs_charset: UTF-8 +info_end +directory: subdir1 +mtime: 1288121499 +begin: subdir1 +songList begin +key: song4.mp3 +file: subdir1/song4.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +key: song5.mp3 +file: subdir1/song5.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +songList end +end: subdir1 +directory: subdir2 +mtime: 1288121499 +begin: subdir2 +songList begin +key: song6.mp3 +file: subdir2/song6.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +key: song7.mp3 +file: subdir2/song7.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +songList end +end: subdir2 +songList begin +key: song1.mp3 +file: /song1.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +key: song2.mp3 +file: /song2.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +key: song3.mp3 +file: /song3.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +songList end diff --git a/tests/data/scanner/empty_cache b/tests/data/scanner/empty_cache new file mode 100644 index 00000000..3c466a32 --- /dev/null +++ b/tests/data/scanner/empty_cache @@ -0,0 +1,6 @@ +info_begin +mpd_version: 0.15.4 +fs_charset: UTF-8 +info_end +songList begin +songList end diff --git a/tests/data/scanner/simple_cache b/tests/data/scanner/simple_cache new file mode 100644 index 00000000..db11c324 --- /dev/null +++ b/tests/data/scanner/simple_cache @@ -0,0 +1,15 @@ +info_begin +mpd_version: 0.15.4 +fs_charset: UTF-8 +info_end +songList begin +key: song1.mp3 +file: /song1.mp3 +Time: 5 +Artist: name +Title: trackname +Album: albumname +Track: 01/02 +Date: 2006 +mtime: 1288121370 +songList end From b76ae84af273413b2022f7bb7908fb33f149c3e6 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 23:13:45 +0200 Subject: [PATCH 030/235] Add basic scanner translator and test --- mopidy/scanner.py | 28 ++++++++++++++++++++++++++++ tests/scanner.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 914f431e..288c8fd1 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -6,10 +6,38 @@ pygst.require('0.10') import gst from os.path import abspath +import datetime import sys import threading from mopidy.utils.path import path_to_uri, find_files +from mopidy.models import Track, Artist, Album + +def translator(data): + album = Album( + name=data['album'], + num_tracks=data['track-count'], + ) + + artist = Artist( + name=data['artist'], + ) + + date = datetime.date( + data['date'].year, + data['date'].month, + data['date'].day, + ) + + return Track( + uri=data['uri'], + name=data['title'], + album=album, + artists=[artist], + date=date, + track_no=data['track-number'], + ) + class Scanner(object): def __init__(self, folder, data_callback, error_callback=None): diff --git a/tests/scanner.py b/tests/scanner.py index 091e8846..a30ca9de 100644 --- a/tests/scanner.py +++ b/tests/scanner.py @@ -1,9 +1,45 @@ import unittest +from datetime import date -from mopidy.scanner import Scanner +from mopidy.scanner import Scanner, translator +from mopidy.models import Track, Artist, Album from tests import data_folder +class FakeGstDate(object): + def __init__(self, year, month, day): + self.year = year + self.month = month + self.day = day + +class TranslatorTest(unittest.TestCase): + def test_basic_data(self): + data = { + 'uri': 'uri', + 'album': u'albumname', + 'track-number': 1, + 'artist': u'name', + 'title': u'trackname', + 'track-count': 2, + 'date': FakeGstDate(2006, 1, 1,), + 'container-format': u'ID3 tag', + # length etc? + } + + expected = Track( + uri='uri', + name='trackname', + album=Album(name='albumname', num_tracks=2), + artists=[Artist(name='name')], + date=date(2006, 1, 1), + track_no=1, + ) + + actual = translator(data) + + self.assertEqual(expected, actual) + + class ScannerTest(unittest.TestCase): def setUp(self): self.errors = {} From 9098f3104cf7b3825376feff0569cd7bd5dd3a47 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 23:14:08 +0200 Subject: [PATCH 031/235] Rename scanner test filename --- tests/{scanner.py => scanner_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{scanner.py => scanner_test.py} (100%) diff --git a/tests/scanner.py b/tests/scanner_test.py similarity index 100% rename from tests/scanner.py rename to tests/scanner_test.py From 7e026174225b8516ee084b82c37bc2466cb124be Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 23:34:10 +0200 Subject: [PATCH 032/235] Move test data in translator test --- tests/scanner_test.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index a30ca9de..7ba6d56c 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -13,8 +13,8 @@ class FakeGstDate(object): self.day = day class TranslatorTest(unittest.TestCase): - def test_basic_data(self): - data = { + def setUp(self): + self.data = { 'uri': 'uri', 'album': u'albumname', 'track-number': 1, @@ -26,6 +26,7 @@ class TranslatorTest(unittest.TestCase): # length etc? } + def test_basic_data(self): expected = Track( uri='uri', name='trackname', @@ -34,10 +35,7 @@ class TranslatorTest(unittest.TestCase): date=date(2006, 1, 1), track_no=1, ) - - actual = translator(data) - - self.assertEqual(expected, actual) + self.assertEqual(expected, translator(self.data)) class ScannerTest(unittest.TestCase): From 045a5a58c54803541fe77f4f02af42b506fe1a98 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Oct 2010 23:41:12 +0200 Subject: [PATCH 033/235] Move uri to scanner data --- mopidy/scanner.py | 5 +++-- tests/scanner_test.py | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 288c8fd1..f3792d5a 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -57,8 +57,9 @@ class Scanner(object): def process_tags(self, bus, message): data = message.parse_tag() - uri = self.uribin.get_property('uri') - self.data_callback(uri, dict([(k, data[k]) for k in data.keys()])) + data = dict([(k, data[k]) for k in data.keys()]) + data['uri'] = self.uribin.get_property('uri') + self.data_callback(data) self.next_uri() def process_error(self, bus, message): diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 7ba6d56c..b3f214a9 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -52,8 +52,8 @@ class ScannerTest(unittest.TestCase): name = data_folder(name) self.assertEqual(self.data[name][key], value) - def data_callback(self, uri, data): - uri = uri[len('file://'):] + def data_callback(self, data): + uri = data['uri'][len('file://'):] self.data[uri] = data def error_callback(self, uri, errors): @@ -68,6 +68,11 @@ class ScannerTest(unittest.TestCase): self.scan('scanner/simple') self.assert_(not self.errors) + def test_uri_is_set(self): + self.scan('scanner/simple') + self.check('scanner/simple/song1.mp3', 'uri', 'file://' + + data_folder('scanner/simple/song1.mp3')) + def test_artist_is_set(self): self.scan('scanner/simple') self.check('scanner/simple/song1.mp3', 'artist', 'name') From d6516915e5e8f59b9cb21351469fa41ca8dbcdd1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 27 Oct 2010 00:15:08 +0200 Subject: [PATCH 034/235] Setup proper pipeline for handling of files in scanner --- mopidy/scanner.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index f3792d5a..937ee2b1 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -46,8 +46,14 @@ class Scanner(object): self.error_callback = error_callback self.loop = gobject.MainLoop() + fakesink = gst.element_factory_make('fakesink') + pad = fakesink.get_pad('sink') + self.uribin = gst.element_factory_make('uridecodebin') + self.uribin.connect('pad-added', self.process_new_pad, pad) + self.pipe = gst.element_factory_make('pipeline') + self.pipe.add(fakesink) self.pipe.add(self.uribin) bus = self.pipe.get_bus() @@ -55,7 +61,13 @@ class Scanner(object): bus.connect('message::tag', self.process_tags) bus.connect('message::error', self.process_error) + def process_new_pad(self, source, pad, target_pad): + pad.link(target_pad) + def process_tags(self, bus, message): + # Block for state change so that duration can be safely determined + self.pipe.get_state() + data = message.parse_tag() data = dict([(k, data[k]) for k in data.keys()]) data['uri'] = self.uribin.get_property('uri') From 79887c1988b41584574042f5147a88dae070cc7c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 27 Oct 2010 00:15:26 +0200 Subject: [PATCH 035/235] Add duration to scanner data --- mopidy/scanner.py | 8 ++++++++ tests/scanner_test.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 937ee2b1..79d6c036 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -71,6 +71,7 @@ class Scanner(object): data = message.parse_tag() data = dict([(k, data[k]) for k in data.keys()]) data['uri'] = self.uribin.get_property('uri') + data['duration'] = self.get_duration() self.data_callback(data) self.next_uri() @@ -81,6 +82,13 @@ class Scanner(object): self.error_callback(uri, errors) self.next_uri() + def get_duration(self): + try: + return self.pipe.query_duration( + gst.FORMAT_TIME, None)[0] // gst.MSECOND + except gst.QueryError: + return None + def next_uri(self): if not self.uris: return self.stop() diff --git a/tests/scanner_test.py b/tests/scanner_test.py index b3f214a9..d47cc7de 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -73,6 +73,10 @@ class ScannerTest(unittest.TestCase): self.check('scanner/simple/song1.mp3', 'uri', 'file://' + data_folder('scanner/simple/song1.mp3')) + def test_duration_is_set(self): + self.scan('scanner/simple') + self.check('scanner/simple/song1.mp3', 'duration', 4680) + def test_artist_is_set(self): self.scan('scanner/simple') self.check('scanner/simple/song1.mp3', 'artist', 'name') From 889b8fdb43eb77a7a62ed0b551170108883024cc Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 27 Oct 2010 00:17:03 +0200 Subject: [PATCH 036/235] Add length to scanner translator --- mopidy/scanner.py | 1 + tests/scanner_test.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 79d6c036..ccc3ce9c 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -36,6 +36,7 @@ def translator(data): artists=[artist], date=date, track_no=data['track-number'], + length=data['duration'], ) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index d47cc7de..4b13f47c 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -23,7 +23,7 @@ class TranslatorTest(unittest.TestCase): 'track-count': 2, 'date': FakeGstDate(2006, 1, 1,), 'container-format': u'ID3 tag', - # length etc? + 'duration': 4531, } def test_basic_data(self): @@ -34,6 +34,7 @@ class TranslatorTest(unittest.TestCase): artists=[Artist(name='name')], date=date(2006, 1, 1), track_no=1, + length=4531, ) self.assertEqual(expected, translator(self.data)) From 0997cc1c58e84217d8727ae3583a1beade45a55a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 27 Oct 2010 10:30:51 +0200 Subject: [PATCH 037/235] Proof of concept: Password authentication We need to at least support the 'commands' command when not authenticated. --- mopidy/frontends/mpd/session.py | 17 +++++++++++++++++ mopidy/settings.py | 5 +++++ 2 files changed, 22 insertions(+) diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py index 580b5905..3ad80c92 100644 --- a/mopidy/frontends/mpd/session.py +++ b/mopidy/frontends/mpd/session.py @@ -2,6 +2,7 @@ import asynchat import logging import multiprocessing +from mopidy import settings from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION from mopidy.utils.log import indent from mopidy.utils.process import pickle_connection @@ -22,6 +23,7 @@ class MpdSession(asynchat.async_chat): self.client_port = client_socket_address[1] self.core_queue = core_queue self.input_buffer = [] + self.authenticated = False self.set_terminator(LINE_TERMINATOR.encode(ENCODING)) def start(self): @@ -46,6 +48,9 @@ class MpdSession(asynchat.async_chat): def handle_request(self, request): """Handle request by sending it to the MPD frontend.""" + if not self.authenticated: + self.authenticated = self.check_password(request) + return my_end, other_end = multiprocessing.Pipe() self.core_queue.put({ 'to': 'frontend', @@ -69,3 +74,15 @@ class MpdSession(asynchat.async_chat): output = u'%s%s' % (output, LINE_TERMINATOR) data = output.encode(ENCODING) self.push(data) + + def check_password(self, request): + if not settings.MPD_SERVER_PASSWORD: + return True + if request == 'password "%s"' % settings.MPD_SERVER_PASSWORD: + self.send_response('OK') + return True + command = request.split(' ')[0] + self.send_response( + "ACK [4@0] {%s} " % command + + "you don't have permission for \"%s\"" % command) + return False diff --git a/mopidy/settings.py b/mopidy/settings.py index c9d7b9fc..da08584e 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -164,6 +164,11 @@ OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput' #: Listens on all interfaces, both IPv4 and IPv6. MPD_SERVER_HOSTNAME = u'127.0.0.1' +#: Password required for connecting to the MPD server. +#: +#: Default: :class:`False`, which means no password required. +MPD_SERVER_PASSWORD = False + #: Which TCP port Mopidy's MPD server should listen to. #: #: Default: 6600 From 3178ef46268f664bc09da19246ec9ab30206c41a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 00:02:51 +0200 Subject: [PATCH 038/235] Move get_state to duration call to fix intermittent errors --- mopidy/scanner.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index ccc3ce9c..991a0864 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -66,9 +66,6 @@ class Scanner(object): pad.link(target_pad) def process_tags(self, bus, message): - # Block for state change so that duration can be safely determined - self.pipe.get_state() - data = message.parse_tag() data = dict([(k, data[k]) for k in data.keys()]) data['uri'] = self.uribin.get_property('uri') @@ -84,6 +81,7 @@ class Scanner(object): self.next_uri() def get_duration(self): + self.pipe.get_state() try: return self.pipe.query_duration( gst.FORMAT_TIME, None)[0] // gst.MSECOND From c247e1455b9fc0fe42eaf4e2f2dfab2aedcbd6ba Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 00:03:14 +0200 Subject: [PATCH 039/235] Ensure that state is null on exit --- mopidy/scanner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 991a0864..6537f35a 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -101,4 +101,5 @@ class Scanner(object): self.loop.run() def stop(self): + self.pipe.set_state(gst.STATE_NULL) self.loop.quit() From 2ebaa38ed92e5fc7a9422ea52abb6f324936b2e7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 00:40:38 +0200 Subject: [PATCH 040/235] Add other media test and set caps to limit to audio for scanner --- mopidy/scanner.py | 2 ++ tests/data/scanner/image/test.png | Bin 0 -> 176 bytes tests/scanner_test.py | 4 ++++ 3 files changed, 6 insertions(+) create mode 100644 tests/data/scanner/image/test.png diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 6537f35a..491fc00b 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -47,11 +47,13 @@ class Scanner(object): self.error_callback = error_callback self.loop = gobject.MainLoop() + caps = gst.Caps('audio/x-raw-int') fakesink = gst.element_factory_make('fakesink') pad = fakesink.get_pad('sink') self.uribin = gst.element_factory_make('uridecodebin') self.uribin.connect('pad-added', self.process_new_pad, pad) + self.uribin.set_property('caps', caps) self.pipe = gst.element_factory_make('pipeline') self.pipe.add(fakesink) diff --git a/tests/data/scanner/image/test.png b/tests/data/scanner/image/test.png new file mode 100644 index 0000000000000000000000000000000000000000..2aaf9c3ddb4c715b788d32514300b0484d5b7156 GIT binary patch literal 176 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1SFYWcSQjy#^NA%Cx&(BWL^R}oCO|{#S9GG z!XV7ZFl&wkP>{XE)7O>#7MHZBfvD8_ccDNb$r9Iy66gHf+|;}h2Ir#G#FEq$h4Rdj z3gTe~DWM4fc^EG= literal 0 HcmV?d00001 diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 4b13f47c..d6639ce1 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -89,3 +89,7 @@ class ScannerTest(unittest.TestCase): def test_track_is_set(self): self.scan('scanner/simple') self.check('scanner/simple/song1.mp3', 'title', 'trackname') + + def test_other_media_is_ignored(self): + self.scan('scanner/image') + self.assert_(self.errors) From d9d393ac218ffa176d17ab1e6d0e957331b8ecb4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 00:47:18 +0200 Subject: [PATCH 041/235] Ensure that scanner does not die on non-existant folders --- mopidy/scanner.py | 5 +++-- tests/scanner_test.py | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 491fc00b..3d5507dc 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -99,8 +99,9 @@ class Scanner(object): self.pipe.set_state(gst.STATE_PAUSED) def start(self): - self.next_uri() - self.loop.run() + if self.uris: + self.next_uri() + self.loop.run() def stop(self): self.pipe.set_state(gst.STATE_NULL) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index d6639ce1..895c73a0 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -90,6 +90,10 @@ class ScannerTest(unittest.TestCase): self.scan('scanner/simple') self.check('scanner/simple/song1.mp3', 'title', 'trackname') + def test_nonexistant_folder_does_not_fail(self): + self.scan('scanner/does-not-exist') + self.assert_(not self.errors) + def test_other_media_is_ignored(self): self.scan('scanner/image') self.assert_(self.errors) From 81da02187f3e5cad6dbcde00e5bde69e8ba08707 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 01:04:02 +0200 Subject: [PATCH 042/235] Start refactoring translator --- mopidy/scanner.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 3d5507dc..1f990d79 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -14,30 +14,29 @@ from mopidy.utils.path import path_to_uri, find_files from mopidy.models import Track, Artist, Album def translator(data): - album = Album( - name=data['album'], - num_tracks=data['track-count'], - ) + album_kwargs = {} + album_kwargs['name'] = data['album'] + album_kwargs['num_tracks'] = data['track-count'] - artist = Artist( - name=data['artist'], - ) + artist_kwargs = {} + artist_kwargs['name'] =data['artist'] - date = datetime.date( - data['date'].year, - data['date'].month, - data['date'].day, - ) + date = data['date'] + date = datetime.date(date.year, date.month, date.day) - return Track( - uri=data['uri'], - name=data['title'], - album=album, - artists=[artist], - date=date, - track_no=data['track-number'], - length=data['duration'], - ) + track_kwargs = {} + track_kwargs['uri'] = data['uri'] + track_kwargs['name'] = data['title'] + track_kwargs['album'] = Album(**album_kwargs) + track_kwargs['artists'] = [Artist(**artist_kwargs)] + track_kwargs['date'] = date + + if 'track-number' in data: + track_kwargs['track_no'] = data['track-number'] + + track_kwargs['length'] = data['duration'] + + return Track(**track_kwargs) class Scanner(object): From 766c447c71b8da13c6c11c1abab9b89719ffe316 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 01:04:18 +0200 Subject: [PATCH 043/235] Refactor translator test --- tests/scanner_test.py | 44 +++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 895c73a0..104235a6 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -26,18 +26,42 @@ class TranslatorTest(unittest.TestCase): 'duration': 4531, } - def test_basic_data(self): - expected = Track( - uri='uri', - name='trackname', - album=Album(name='albumname', num_tracks=2), - artists=[Artist(name='name')], - date=date(2006, 1, 1), - track_no=1, - length=4531, - ) + self.album = { + 'name': 'albumname', + 'num_tracks': 2, + } + + self.artist = { + 'name': 'name', + } + + self.track = { + 'uri': 'uri', + 'name': 'trackname', + 'date': date(2006, 1, 1), + 'track_no': 1, + 'length': 4531, + } + + def build_track(self): + if self.album: + self.track['album'] = Album(**self.album) + if self.artist: + self.track['artists'] = [Artist(**self.artist)] + return Track(**self.track) + + def check(self): + expected = self.build_track() + actual = translator(self.data) self.assertEqual(expected, translator(self.data)) + def test_basic_data(self): + self.check() + + def test_missing_track_number(self): + del self.track['track_no'] + del self.data['track-number'] + self.check() class ScannerTest(unittest.TestCase): def setUp(self): From ab97f780608e09f671617c8d628d12e1edeecc15 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 01:12:20 +0200 Subject: [PATCH 044/235] Test all optional translator values --- mopidy/scanner.py | 33 ++++++++++++++++++++------------- tests/scanner_test.py | 33 ++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 1f990d79..2222d3a0 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -15,26 +15,33 @@ from mopidy.models import Track, Artist, Album def translator(data): album_kwargs = {} - album_kwargs['name'] = data['album'] - album_kwargs['num_tracks'] = data['track-count'] - artist_kwargs = {} - artist_kwargs['name'] =data['artist'] - - date = data['date'] - date = datetime.date(date.year, date.month, date.day) - track_kwargs = {} - track_kwargs['uri'] = data['uri'] - track_kwargs['name'] = data['title'] - track_kwargs['album'] = Album(**album_kwargs) - track_kwargs['artists'] = [Artist(**artist_kwargs)] - track_kwargs['date'] = date + + if 'album' in data: + album_kwargs['name'] = data['album'] + + if 'track-count' in data: + album_kwargs['num_tracks'] = data['track-count'] + + if 'artist' in data: + artist_kwargs['name'] =data['artist'] + + if 'date' in data: + date = data['date'] + date = datetime.date(date.year, date.month, date.day) + track_kwargs['date'] = date + + if 'title' in data: + track_kwargs['name'] = data['title'] if 'track-number' in data: track_kwargs['track_no'] = data['track-number'] + track_kwargs['uri'] = data['uri'] track_kwargs['length'] = data['duration'] + track_kwargs['album'] = Album(**album_kwargs) + track_kwargs['artists'] = [Artist(**artist_kwargs)] return Track(**track_kwargs) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 104235a6..b33ed99a 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -44,10 +44,8 @@ class TranslatorTest(unittest.TestCase): } def build_track(self): - if self.album: - self.track['album'] = Album(**self.album) - if self.artist: - self.track['artists'] = [Artist(**self.artist)] + self.track['album'] = Album(**self.album) + self.track['artists'] = [Artist(**self.artist)] return Track(**self.track) def check(self): @@ -59,8 +57,33 @@ class TranslatorTest(unittest.TestCase): self.check() def test_missing_track_number(self): - del self.track['track_no'] del self.data['track-number'] + del self.track['track_no'] + self.check() + + def test_missing_track_count(self): + del self.data['track-count'] + del self.album['num_tracks'] + self.check() + + def test_missing_track_name(self): + del self.data['title'] + del self.track['name'] + self.check() + + def test_missing_album_name(self): + del self.data['album'] + del self.album['name'] + self.check() + + def test_missing_artist_name(self): + del self.data['artist'] + del self.artist['name'] + self.check() + + def test_missing_date(self): + del self.data['date'] + del self.track['date'] self.check() class ScannerTest(unittest.TestCase): From 9cb9cf3e0cbbbb19398b74a02584aa30cf8206eb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Oct 2010 01:28:41 +0200 Subject: [PATCH 045/235] Clean start scanner code a tiny bit --- mopidy/scanner.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 2222d3a0..ccdab24b 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -105,9 +105,10 @@ class Scanner(object): self.pipe.set_state(gst.STATE_PAUSED) def start(self): - if self.uris: - self.next_uri() - self.loop.run() + if not self.uris: + return + self.next_uri() + self.loop.run() def stop(self): self.pipe.set_state(gst.STATE_NULL) From 3e595213ac26c8770b02bf42620bff3221fbde39 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 19:34:33 +0200 Subject: [PATCH 046/235] Ensure that result has exact right number of elements --- tests/frontends/mpd/serializer_test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 0e0f8183..6bd61e12 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -14,6 +14,7 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Album', '') in result) self.assert_(('Track', 0) in result) self.assert_(('Date', '') in result) + self.assertEqual(len(result), 7) def test_mpd_format_for_nonempty_track(self): track = Track( @@ -35,11 +36,12 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Date', dt.date(1977, 1, 1)) in result) self.assert_(('Pos', 9) in result) self.assert_(('Id', 122) in result) + self.assertEqual(len(result), 9) def test_mpd_format_artists(self): track = Track(artists=[Artist(name=u'ABBA'), Artist(name=u'Beatles')]) - self.assertEqual(translator.track_artists_to_mpd_format(track), - u'ABBA, Beatles') + translated = translator.track_artists_to_mpd_format(track) + self.assertEqual(translated, u'ABBA, Beatles') class PlaylistMpdFormatTest(unittest.TestCase): From 02df8ca033051d08a724d43dbfea1aec0a6fbd6f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 20:04:53 +0200 Subject: [PATCH 047/235] Start writting tracks_to_tag_cache_format --- mopidy/frontends/mpd/translator.py | 15 +++++++++++++++ tests/frontends/mpd/serializer_test.py | 21 ++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 07a58dd3..44003dfd 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -1,3 +1,5 @@ +from mopidy.frontends.mpd import protocol + def track_to_mpd_format(track, position=None, cpid=None): """ Format track for output to MPD client. @@ -72,3 +74,16 @@ def playlist_to_mpd_format(playlist, *args, **kwargs): Arguments as for :func:`tracks_to_mpd_format`, except the first one. """ return tracks_to_mpd_format(playlist.tracks, *args, **kwargs) + +def tracks_to_tag_cache_format(tracks): + result = [ + ('info_begin',), + ('mpd_version', protocol.VERSION), + ('fs_charset', protocol.ENCODING), + ('info_end',) + ] + + result.append(('songList begin',)) + result.append(('songList end',)) + + return result diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 6bd61e12..1729df70 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -1,7 +1,7 @@ import datetime as dt import unittest -from mopidy.frontends.mpd import translator +from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, Playlist, Track class TrackMpdFormatTest(unittest.TestCase): @@ -57,3 +57,22 @@ class PlaylistMpdFormatTest(unittest.TestCase): result = translator.playlist_to_mpd_format(playlist, 1, 2) self.assertEqual(len(result), 1) self.assertEqual(dict(result[0])['Track'], 2) + + +class TracksToTagCacheFormatTest(unittest.TestCase): + header_length = 4 + + def check_headers(self, result): + self.assert_(('info_begin',) in result) + self.assert_(('mpd_version', protocol.VERSION) in result) + self.assert_(('fs_charset', protocol.ENCODING) in result) + self.assert_(('info_end',) in result) + + def test_empty_tag_cache(self): + result = translator.tracks_to_tag_cache_format([]) + self.check_headers(result) + + self.assert_(('songList begin',) in result) + self.assert_(('songList end',) in result) + self.assertEqual(len(result), self.header_length+2) + From 44012b30b02acf9b99261383bf72cce90557aa98 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 20:32:50 +0200 Subject: [PATCH 048/235] Add uri_to_mpd_relative_path --- mopidy/frontends/mpd/translator.py | 8 ++++++++ tests/frontends/mpd/serializer_test.py | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 44003dfd..28d1e08b 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -1,4 +1,8 @@ +import re + +from mopidy import settings from mopidy.frontends.mpd import protocol +from mopidy.utils.path import path_to_uri def track_to_mpd_format(track, position=None, cpid=None): """ @@ -75,6 +79,10 @@ def playlist_to_mpd_format(playlist, *args, **kwargs): """ return tracks_to_mpd_format(playlist.tracks, *args, **kwargs) +def uri_to_mpd_relative_path(uri): + path = path_to_uri(settings.LOCAL_MUSIC_FOLDER) + return re.sub('^' + re.escape(path), '', uri) + def tracks_to_tag_cache_format(tracks): result = [ ('info_begin',), diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 1729df70..cd9320b6 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -1,6 +1,7 @@ import datetime as dt import unittest +from mopidy import settings from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, Playlist, Track @@ -59,6 +60,19 @@ class PlaylistMpdFormatTest(unittest.TestCase): self.assertEqual(dict(result[0])['Track'], 2) +class UriToMpdRelativePathTest(unittest.TestCase): + def setUp(self): + settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' + + def tearDown(self): + settings.runtime.clear() + + def test_file_gets_stripped(self): + uri = 'file:///dir/subdir/music/album/song.mp3' + result = translator.uri_to_mpd_relative_path(uri) + self.assertEqual('/music/album/song.mp3', result) + + class TracksToTagCacheFormatTest(unittest.TestCase): header_length = 4 @@ -75,4 +89,3 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.assert_(('songList begin',) in result) self.assert_(('songList end',) in result) self.assertEqual(len(result), self.header_length+2) - From 8240dbbb557e7ef56b51e9d6d04e9b0e35e0c1ab Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 20:36:38 +0200 Subject: [PATCH 049/235] Add docstrings --- mopidy/frontends/mpd/translator.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 28d1e08b..ccd83fde 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -80,10 +80,24 @@ def playlist_to_mpd_format(playlist, *args, **kwargs): return tracks_to_mpd_format(playlist.tracks, *args, **kwargs) def uri_to_mpd_relative_path(uri): + """ + Strip uri and LOCAL_MUSIC_FOLDER part of uri. + + :param uri: the uri + :type uri: string + :rtype: string + """ path = path_to_uri(settings.LOCAL_MUSIC_FOLDER) return re.sub('^' + re.escape(path), '', uri) def tracks_to_tag_cache_format(tracks): + """ + Format list of tracks for output to MPD tag cache + + :param tracks: the tracks + :type tracks: list of :class:`mopidy.models.Track` + :rtype: list of lists of two-tuples + """ result = [ ('info_begin',), ('mpd_version', protocol.VERSION), From 014e29ffa22d010ee572902a371a7c177ff1c078 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 21:15:32 +0200 Subject: [PATCH 050/235] Fix uri_to_mpd_relative_path so that it handles None --- mopidy/frontends/mpd/translator.py | 2 ++ tests/frontends/mpd/serializer_test.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index ccd83fde..f66c64a7 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -87,6 +87,8 @@ def uri_to_mpd_relative_path(uri): :type uri: string :rtype: string """ + if uri is None: + return '' path = path_to_uri(settings.LOCAL_MUSIC_FOLDER) return re.sub('^' + re.escape(path), '', uri) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index cd9320b6..960e5ebd 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -67,6 +67,11 @@ class UriToMpdRelativePathTest(unittest.TestCase): def tearDown(self): settings.runtime.clear() + def test_none_file_returns_empty_string(self): + uri = 'file:///dir/subdir/music/album/song.mp3' + result = translator.uri_to_mpd_relative_path(None) + self.assertEqual('', result) + def test_file_gets_stripped(self): uri = 'file:///dir/subdir/music/album/song.mp3' result = translator.uri_to_mpd_relative_path(uri) From cf516c5ef04d1a2a709f384d0df3132a72f5f741 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 21:17:08 +0200 Subject: [PATCH 051/235] Ensure that mpd_format_track handles file: properly --- mopidy/frontends/mpd/translator.py | 2 +- tests/frontends/mpd/serializer_test.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index f66c64a7..49fc064d 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -17,7 +17,7 @@ def track_to_mpd_format(track, position=None, cpid=None): :rtype: list of two-tuples """ result = [ - ('file', track.uri or ''), + ('file', uri_to_mpd_relative_path(track.uri) or ''), ('Time', track.length and (track.length // 1000) or 0), ('Artist', track_artists_to_mpd_format(track)), ('Title', track.name or ''), diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 960e5ebd..af12a0de 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -6,6 +6,12 @@ from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, Playlist, Track class TrackMpdFormatTest(unittest.TestCase): + def setUp(self): + settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' + + def tearDown(self): + settings.runtime.clear() + def test_mpd_format_for_empty_track(self): result = translator.track_to_mpd_format(Track()) self.assert_(('file', '') in result) @@ -17,6 +23,12 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Date', '') in result) self.assertEqual(len(result), 7) + def test_mpd_format_track_uses_uri_to_mpd_relative_path(self): + track = Track(uri='file:///dir/subdir/song.mp3') + path = dict(translator.track_to_mpd_format(track))['file'] + correct_path = translator.uri_to_mpd_relative_path(track.uri) + self.assertEqual(path, correct_path) + def test_mpd_format_for_nonempty_track(self): track = Track( uri=u'a uri', From 151b1c3b4ebed89c195b5352b63f3b05f4f3a49a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 21:23:15 +0200 Subject: [PATCH 052/235] Add explicit tests for position and cpid in track_to_mpd_format --- tests/frontends/mpd/serializer_test.py | 50 +++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index af12a0de..09db3394 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -23,6 +23,19 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Date', '') in result) self.assertEqual(len(result), 7) + def test_mpd_format_with_position(self): + result = translator.track_to_mpd_format(Track(), position=1) + self.assert_(('Pos', 1) not in result) + + def test_mpd_format_with_cpid(self): + result = translator.track_to_mpd_format(Track(), cpid=1) + self.assert_(('Id', 1) not in result) + + def test_mpd_format_with_position_and_cpid(self): + result = translator.track_to_mpd_format(Track(), position=1, cpid=2) + self.assert_(('Pos', 1) in result) + self.assert_(('Id', 2) in result) + def test_mpd_format_track_uses_uri_to_mpd_relative_path(self): track = Track(uri='file:///dir/subdir/song.mp3') path = dict(translator.track_to_mpd_format(track))['file'] @@ -91,18 +104,45 @@ class UriToMpdRelativePathTest(unittest.TestCase): class TracksToTagCacheFormatTest(unittest.TestCase): - header_length = 4 + def setUp(self): + settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' + + def tearDown(self): + settings.runtime.clear() def check_headers(self, result): self.assert_(('info_begin',) in result) self.assert_(('mpd_version', protocol.VERSION) in result) self.assert_(('fs_charset', protocol.ENCODING) in result) self.assert_(('info_end',) in result) + return result[4:] + + def check_song_list(self, result): + self.assertEqual(('songList begin',), result[0]) + self.assertEqual(('songList end',), result[-1]) + return result[1:-1] def test_empty_tag_cache(self): result = translator.tracks_to_tag_cache_format([]) - self.check_headers(result) + result = self.check_headers(result) + result = self.check_song_list(result) + self.assertEqual(len(result), 0) - self.assert_(('songList begin',) in result) - self.assert_(('songList end',) in result) - self.assertEqual(len(result), self.header_length+2) + def test_simple_tag_cache_has_header(self): + track = Track(uri='file:///dir/subdir/song.mp3') + result = translator.tracks_to_tag_cache_format([track]) + result = self.check_headers(result) + result = self.check_song_list(result) + self.assertEqual(len(result), 0) + + def test_simple_tag_cache_has_header(self): + track = Track(uri='file:///dir/subdir/song.mp3') + formated = translator.track_to_mpd_format(track) + formated.insert(0, ('key', 'song.mp3')) + + result = translator.tracks_to_tag_cache_format([track]) + result = self.check_headers(result) + result = self.check_song_list(result) + + for a, b in zip(result, formated): + self.assertEqual(a, b) From 488ac284311e7beb8df66d7c6f147b3784e3aeeb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 21:23:50 +0200 Subject: [PATCH 053/235] Add key parameter to track_to_mpd_format --- mopidy/frontends/mpd/translator.py | 5 ++++- tests/frontends/mpd/serializer_test.py | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 49fc064d..87708da5 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -1,10 +1,11 @@ +import os import re from mopidy import settings from mopidy.frontends.mpd import protocol from mopidy.utils.path import path_to_uri -def track_to_mpd_format(track, position=None, cpid=None): +def track_to_mpd_format(track, position=None, cpid=None, key=None): """ Format track for output to MPD client. @@ -32,6 +33,8 @@ def track_to_mpd_format(track, position=None, cpid=None): if position is not None and cpid is not None: result.append(('Pos', position)) result.append(('Id', cpid)) + if key is not None: + result.insert(0, ('key', key)) return result def track_artists_to_mpd_format(track): diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 09db3394..ba4e8f18 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -36,6 +36,10 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Pos', 1) in result) self.assert_(('Id', 2) in result) + def test_mpd_format_with_key(self): + result = translator.track_to_mpd_format(Track(), key='file.mp3') + self.assert_(('key', 'file.mp3') in result) + def test_mpd_format_track_uses_uri_to_mpd_relative_path(self): track = Track(uri='file:///dir/subdir/song.mp3') path = dict(translator.track_to_mpd_format(track))['file'] From b2cb3136b25db39c3d169fd6bff397817bec27c5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 21:27:40 +0200 Subject: [PATCH 054/235] Rename tests --- tests/frontends/mpd/serializer_test.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index ba4e8f18..99174ad3 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -12,7 +12,7 @@ class TrackMpdFormatTest(unittest.TestCase): def tearDown(self): settings.runtime.clear() - def test_mpd_format_for_empty_track(self): + def test_track_to_mpd_format_for_empty_track(self): result = translator.track_to_mpd_format(Track()) self.assert_(('file', '') in result) self.assert_(('Time', 0) in result) @@ -23,30 +23,30 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Date', '') in result) self.assertEqual(len(result), 7) - def test_mpd_format_with_position(self): + def test_track_to_mpd_format_with_position(self): result = translator.track_to_mpd_format(Track(), position=1) self.assert_(('Pos', 1) not in result) - def test_mpd_format_with_cpid(self): + def test_track_to_mpd_format_with_cpid(self): result = translator.track_to_mpd_format(Track(), cpid=1) self.assert_(('Id', 1) not in result) - def test_mpd_format_with_position_and_cpid(self): + def test_track_to_mpd_format_with_position_and_cpid(self): result = translator.track_to_mpd_format(Track(), position=1, cpid=2) self.assert_(('Pos', 1) in result) self.assert_(('Id', 2) in result) - def test_mpd_format_with_key(self): + def test_track_to_mpd_format_with_key(self): result = translator.track_to_mpd_format(Track(), key='file.mp3') self.assert_(('key', 'file.mp3') in result) - def test_mpd_format_track_uses_uri_to_mpd_relative_path(self): + def test_track_to_mpd_format_track_uses_uri_to_mpd_relative_path(self): track = Track(uri='file:///dir/subdir/song.mp3') path = dict(translator.track_to_mpd_format(track))['file'] correct_path = translator.uri_to_mpd_relative_path(track.uri) self.assertEqual(path, correct_path) - def test_mpd_format_for_nonempty_track(self): + def test_track_to_mpd_format_for_nonempty_track(self): track = Track( uri=u'a uri', artists=[Artist(name=u'an artist')], @@ -68,7 +68,7 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Id', 122) in result) self.assertEqual(len(result), 9) - def test_mpd_format_artists(self): + def test_track_artists_to_mpd_format(self): track = Track(artists=[Artist(name=u'ABBA'), Artist(name=u'Beatles')]) translated = translator.track_artists_to_mpd_format(track) self.assertEqual(translated, u'ABBA, Beatles') From a8a447c47fe59d46b596aa54c391473acb165813 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 21:36:11 +0200 Subject: [PATCH 055/235] Use key=True for track_to_mpd_format generation --- mopidy/frontends/mpd/translator.py | 6 ++++-- tests/frontends/mpd/serializer_test.py | 12 +++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 87708da5..3f7a6adf 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -33,8 +33,8 @@ def track_to_mpd_format(track, position=None, cpid=None, key=None): if position is not None and cpid is not None: result.append(('Pos', position)) result.append(('Id', cpid)) - if key is not None: - result.insert(0, ('key', key)) + if key and track.uri: + result.insert(0, ('key', os.path.basename(track.uri))) return result def track_artists_to_mpd_format(track): @@ -111,6 +111,8 @@ def tracks_to_tag_cache_format(tracks): ] result.append(('songList begin',)) + for track in tracks: + result.extend(track_to_mpd_format(track, key=True)) result.append(('songList end',)) return result diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 99174ad3..4b4bdabb 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -37,7 +37,8 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Id', 2) in result) def test_track_to_mpd_format_with_key(self): - result = translator.track_to_mpd_format(Track(), key='file.mp3') + track = Track(uri='file:///dir/subdir/file.mp3') + result = translator.track_to_mpd_format(track, key=True) self.assert_(('key', 'file.mp3') in result) def test_track_to_mpd_format_track_uses_uri_to_mpd_relative_path(self): @@ -137,16 +138,13 @@ class TracksToTagCacheFormatTest(unittest.TestCase): result = translator.tracks_to_tag_cache_format([track]) result = self.check_headers(result) result = self.check_song_list(result) - self.assertEqual(len(result), 0) - def test_simple_tag_cache_has_header(self): + def test_simple_tag_cache_has_formated_track(self): track = Track(uri='file:///dir/subdir/song.mp3') - formated = translator.track_to_mpd_format(track) - formated.insert(0, ('key', 'song.mp3')) + formated = translator.track_to_mpd_format(track, key=True) result = translator.tracks_to_tag_cache_format([track]) result = self.check_headers(result) result = self.check_song_list(result) - for a, b in zip(result, formated): - self.assertEqual(a, b) + self.assertEqual(result, formated) From 21eadf3dc734c0fa8a3e742cc2c4728447f8a770 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 21:56:20 +0200 Subject: [PATCH 056/235] Refactor tag_cache generation tests --- tests/frontends/mpd/serializer_test.py | 55 +++++++++++++++++--------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 4b4bdabb..de0a6814 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -115,36 +115,53 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def tearDown(self): settings.runtime.clear() - def check_headers(self, result): - self.assert_(('info_begin',) in result) - self.assert_(('mpd_version', protocol.VERSION) in result) - self.assert_(('fs_charset', protocol.ENCODING) in result) - self.assert_(('info_end',) in result) + def consume_headers(self, result): + self.assertEqual(('info_begin',), result[0]) + self.assertEqual(('mpd_version', protocol.VERSION), result[1]) + self.assertEqual(('fs_charset', protocol.ENCODING), result[2]) + self.assertEqual(('info_end',), result[3]) return result[4:] - def check_song_list(self, result): + def consume_song_list(self, result): self.assertEqual(('songList begin',), result[0]) - self.assertEqual(('songList end',), result[-1]) - return result[1:-1] + for i, row in enumerate(result): + if row == ('songList end',): + return result[1:i], result[i+1:] + self.fail("Couldn't find songList end in result") - def test_empty_tag_cache(self): + def test_empty_tag_cache_has_header(self): result = translator.tracks_to_tag_cache_format([]) - result = self.check_headers(result) - result = self.check_song_list(result) + result = self.consume_headers(result) + + def test_empty_tag_cache_has_song_list(self): + result = translator.tracks_to_tag_cache_format([]) + result = self.consume_headers(result) + song_list, result = self.consume_song_list(result) + + self.assertEqual(len(song_list), 0) self.assertEqual(len(result), 0) - def test_simple_tag_cache_has_header(self): + def test_tag_cache_has_header(self): track = Track(uri='file:///dir/subdir/song.mp3') result = translator.tracks_to_tag_cache_format([track]) - result = self.check_headers(result) - result = self.check_song_list(result) + result = self.consume_headers(result) - def test_simple_tag_cache_has_formated_track(self): + 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 = self.consume_headers(result) + song_list, result = self.consume_song_list(result) + + self.assert_(song_list) + self.assertEqual(len(result), 0) + + def test_tag_cache_has_formated_track(self): track = Track(uri='file:///dir/subdir/song.mp3') formated = translator.track_to_mpd_format(track, key=True) - result = translator.tracks_to_tag_cache_format([track]) - result = self.check_headers(result) - result = self.check_song_list(result) - self.assertEqual(result, formated) + result = self.consume_headers(result) + song_list, result = self.consume_song_list(result) + + self.assertEqual(song_list, formated) + self.assertEqual(len(result), 0) From d785b9b14e52d01b5b05564d37423cb49c22a0ef Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 22:16:33 +0200 Subject: [PATCH 057/235] Added uri_to_path with tests --- mopidy/utils/path.py | 8 ++++++++ tests/utils/path_test.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 9220c53d..73d89d49 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -1,6 +1,7 @@ import logging import os import sys +import re import urllib logger = logging.getLogger('mopidy.utils.path') @@ -27,6 +28,13 @@ def path_to_uri(*paths): return 'file:' + urllib.pathname2url(path) return 'file://' + urllib.pathname2url(path) +def uri_to_path(uri): + if sys.platform == 'win32': + path = urllib.url2pathname(re.sub('^file:', '', uri)) + else: + path = urllib.url2pathname(re.sub('^file://', '', uri)) + return path.encode('latin1').decode('utf-8') # Undo double encoding + def find_files(path): if os.path.isfile(path): yield os.path.abspath(path) diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index e0359a16..c60e7833 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -6,7 +6,8 @@ import sys import tempfile import unittest -from mopidy.utils.path import get_or_create_folder, path_to_uri, find_files +from mopidy.utils.path import (get_or_create_folder, + path_to_uri, uri_to_path, find_files) from tests import SkipTest, data_folder @@ -71,6 +72,32 @@ class PathToFileURITest(unittest.TestCase): self.assertEqual(result, u'file:///tmp/%C3%A6%C3%B8%C3%A5') +class UriToPathTest(unittest.TestCase): + def test_simple_uri(self): + if sys.platform == 'win32': + result = uri_to_path('file:///C://WINDOWS/clock.avi') + self.assertEqual(result, u'C:/WINDOWS/clock.avi') + else: + result = uri_to_path('file:///etc/fstab') + self.assertEqual(result, u'/etc/fstab') + + def test_space_in_uri(self): + if sys.platform == 'win32': + result = uri_to_path('file:///C://test%20this') + self.assertEqual(result, u'C:/test this') + else: + result = uri_to_path(u'file:///tmp/test%20this') + self.assertEqual(result, u'/tmp/test this') + + def test_unicode_in_uri(self): + if sys.platform == 'win32': + result = uri_to_path( 'file:///C://%C3%A6%C3%B8%C3%A5') + self.assertEqual(result, u'C:/æøå') + else: + result = uri_to_path(u'file:///tmp/%C3%A6%C3%B8%C3%A5') + self.assertEqual(result, u'/tmp/æøå') + + class FindFilesTest(unittest.TestCase): def find(self, path): return list(find_files(data_folder(path))) From 8d1339ef7f0bd2a38b9aa0e7888c8b81da75c7a0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 22:18:35 +0200 Subject: [PATCH 058/235] Add option to get mtime set in translator --- mopidy/frontends/mpd/translator.py | 7 +++++-- tests/frontends/mpd/serializer_test.py | 8 ++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 3f7a6adf..41d2af5c 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -3,9 +3,9 @@ import re from mopidy import settings from mopidy.frontends.mpd import protocol -from mopidy.utils.path import path_to_uri +from mopidy.utils.path import path_to_uri, uri_to_path -def track_to_mpd_format(track, position=None, cpid=None, key=None): +def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False): """ Format track for output to MPD client. @@ -35,6 +35,9 @@ def track_to_mpd_format(track, position=None, cpid=None, key=None): result.append(('Id', cpid)) if key and track.uri: result.insert(0, ('key', os.path.basename(track.uri))) + if mtime and track.uri: + mtime = os.stat(uri_to_path(track.uri)).st_mtime + result.append(('mtime', int(mtime))) return result def track_artists_to_mpd_format(track): diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index de0a6814..689347fe 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -5,6 +5,8 @@ from mopidy import settings from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, Playlist, Track +from tests import data_folder + class TrackMpdFormatTest(unittest.TestCase): def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' @@ -41,6 +43,12 @@ class TrackMpdFormatTest(unittest.TestCase): result = translator.track_to_mpd_format(track, key=True) self.assert_(('key', 'file.mp3') in result) + def test_track_to_mpd_format_with_mtime(self): + uri = translator.path_to_uri(data_folder('blank.mp3')) + result = translator.track_to_mpd_format(Track(uri=uri), mtime=True) + print result + self.assert_(('mtime', 1288125516) in result) + def test_track_to_mpd_format_track_uses_uri_to_mpd_relative_path(self): track = Track(uri='file:///dir/subdir/song.mp3') path = dict(translator.track_to_mpd_format(track))['file'] From 059ad2d4c91fd99a4c1fcb9f9d8a391420d45519 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 22:20:19 +0200 Subject: [PATCH 059/235] Update track_to_mpd_format docstring with new parameters --- mopidy/frontends/mpd/translator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 41d2af5c..98b3f907 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -15,6 +15,10 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) :type position: integer :param cpid: track's CPID (current playlist ID) :type cpid: integer + :param key: if we should set key + :type key: boolean + :param mtime: if we should set mtime + :type mtime: boolean :rtype: list of two-tuples """ result = [ From a48e88104012c4b4ed8d87af5c0e9e92de8fc713 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 22:49:48 +0200 Subject: [PATCH 060/235] Add split path util --- mopidy/utils/path.py | 10 ++++++++++ tests/utils/path_test.py | 22 +++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 73d89d49..ef331045 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -35,6 +35,16 @@ def uri_to_path(uri): path = urllib.url2pathname(re.sub('^file://', '', uri)) return path.encode('latin1').decode('utf-8') # Undo double encoding +def split_path(path): + parts = [] + while True: + path, part = os.path.split(path) + if part: + parts.insert(0, part) + if not path or path == '/': + break + return parts + def find_files(path): if os.path.isfile(path): yield os.path.abspath(path) diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index c60e7833..2990c6d3 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -7,7 +7,7 @@ import tempfile import unittest from mopidy.utils.path import (get_or_create_folder, - path_to_uri, uri_to_path, find_files) + path_to_uri, uri_to_path, split_path, find_files) from tests import SkipTest, data_folder @@ -98,6 +98,26 @@ class UriToPathTest(unittest.TestCase): self.assertEqual(result, u'/tmp/æøå') +class SplitPathTest(unittest.TestCase): + def test_empty_path(self): + self.assertEqual([], split_path('')) + + def test_single_folder(self): + self.assertEqual(['foo'], split_path('foo')) + + def test_folders(self): + self.assertEqual(['foo', 'bar', 'baz'], split_path('foo/bar/baz')) + + def test_folders(self): + self.assertEqual(['foo', 'bar', 'baz'], split_path('foo/bar/baz')) + + def test_initial_slash_is_ignored(self): + self.assertEqual(['foo', 'bar', 'baz'], split_path('/foo/bar/baz')) + + def test_only_slash(self): + self.assertEqual([], split_path('/')) + + class FindFilesTest(unittest.TestCase): def find(self, path): return list(find_files(data_folder(path))) From 1a6831ab683f75cf9f0fb6d067d5f07871c08f53 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 23:06:58 +0200 Subject: [PATCH 061/235] Add tracks_to_directory_tree helper --- mopidy/frontends/mpd/translator.py | 14 ++++++- tests/frontends/mpd/serializer_test.py | 56 ++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 98b3f907..36a6e7ea 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -3,7 +3,7 @@ import re from mopidy import settings from mopidy.frontends.mpd import protocol -from mopidy.utils.path import path_to_uri, uri_to_path +from mopidy.utils.path import path_to_uri, uri_to_path, split_path def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False): """ @@ -123,3 +123,15 @@ def tracks_to_tag_cache_format(tracks): result.append(('songList end',)) return result + +def tracks_to_directory_tree(tracks): + directories = ({}, []) + for track in tracks: + folder = os.path.dirname(uri_to_path(track.uri)) + current = directories + for part in split_path(folder): + if part not in current[0]: + current[0][part] = ({}, []) + current = current[0][part] + current[1].append(track) + return directories diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 689347fe..5fc610b0 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -173,3 +173,59 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.assertEqual(song_list, formated) self.assertEqual(len(result), 0) + + +class TracksToDirectoryTreeTest(unittest.TestCase): + def setUp(self): + settings.LOCAL_MUSIC_FOLDER = '/' + + def tearDown(self): + settings.runtime.clear() + + def test_no_tracks_gives_emtpy_tree(self): + tree = translator.tracks_to_directory_tree([]) + self.assertEqual(tree, ({}, [])) + + def test_top_level_files(self): + tracks = [ + Track(uri='file:///file1.mp3'), + Track(uri='file:///file2.mp3'), + Track(uri='file:///file3.mp3'), + ] + tree = translator.tracks_to_directory_tree(tracks) + self.assertEqual(tree, ({}, tracks)) + + def test_single_file_in_subdir(self): + tracks = [Track(uri='file:///dir/file1.mp3')] + tree = translator.tracks_to_directory_tree(tracks) + expected = ({'dir': ({}, tracks)}, []) + self.assertEqual(tree, expected) + + def test_single_file_in_sub_subdir(self): + tracks = [Track(uri='file:///dir1/dir2/file1.mp3')] + tree = translator.tracks_to_directory_tree(tracks) + expected = ({'dir1': ({'dir2': ({}, tracks)}, [])}, []) + self.assertEqual(tree, expected) + + def test_complex_file_structure(self): + tracks = [ + Track(uri='file:///file1.mp3'), + Track(uri='file:///dir1/file2.mp3'), + Track(uri='file:///dir1/file3.mp3'), + Track(uri='file:///dir2/file4.mp3'), + Track(uri='file:///dir2/sub/file5.mp3'), + ] + tree = translator.tracks_to_directory_tree(tracks) + expected = ( + { + 'dir1': ({}, [tracks[1], tracks[2]]), + 'dir2': ( + { + 'sub': ({}, [tracks[4]]) + }, + [tracks[3]] + ), + }, + [tracks[0]] + ) + self.assertEqual(tree, expected) From 94db967672f2aa22aa7557e609e7ccdcd4990897 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 23:26:05 +0200 Subject: [PATCH 062/235] Tag cache seems to support directories now --- mopidy/frontends/mpd/translator.py | 15 +++++++++++---- tests/frontends/mpd/serializer_test.py | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 36a6e7ea..6c93e04e 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -116,18 +116,25 @@ def tracks_to_tag_cache_format(tracks): ('fs_charset', protocol.ENCODING), ('info_end',) ] + _add_to_tag_cache(result, *tracks_to_directory_tree(tracks)) + return result + +def _add_to_tag_cache(result, folders, files): + for name, entry in folders.items(): + result.append(('begin', name)) + _add_to_tag_cache(result, *entry) + result.append(('end', name)) result.append(('songList begin',)) - for track in tracks: + for track in files: result.extend(track_to_mpd_format(track, key=True)) result.append(('songList end',)) - return result - def tracks_to_directory_tree(tracks): directories = ({}, []) for track in tracks: - folder = os.path.dirname(uri_to_path(track.uri)) + uri = uri_to_mpd_relative_path(track.uri) + folder = os.path.dirname(uri_to_path(uri)) current = directories for part in split_path(folder): if part not in current[0]: diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 5fc610b0..a4abc5a9 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -137,6 +137,14 @@ class TracksToTagCacheFormatTest(unittest.TestCase): return result[1:i], result[i+1:] self.fail("Couldn't find songList end in result") + def consume_directory(self, result): + self.assertEqual('begin', result[0][0]) + directory = result[0][1] + for i, row in enumerate(result): + if row == ('end', directory): + return result[1:i], result[i+1:] + 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 = self.consume_headers(result) @@ -174,6 +182,16 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.assertEqual(song_list, formated) self.assertEqual(len(result), 0) + def test_tag_cache_suports_directories(self): + track = Track(uri='file:///dir/subdir/folder/song.mp3') + formated = translator.track_to_mpd_format(track, key=True) + result = translator.tracks_to_tag_cache_format([track]) + + result = self.consume_headers(result) + directory, result = self.consume_directory(result) + song_list, result = self.consume_song_list(directory) + + self.assertEqual(song_list, formated) class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): From 6b99416830ec881684cd953ad8b6cf95a7d6ccb3 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 23:43:49 +0200 Subject: [PATCH 063/235] Add test for sub sub dirs in tag cache generator --- tests/frontends/mpd/serializer_test.py | 32 +++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index a4abc5a9..6025109e 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -5,7 +5,7 @@ from mopidy import settings from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, Playlist, Track -from tests import data_folder +from tests import data_folder, SkipTest class TrackMpdFormatTest(unittest.TestCase): def setUp(self): @@ -188,11 +188,37 @@ class TracksToTagCacheFormatTest(unittest.TestCase): result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) - directory, result = self.consume_directory(result) - song_list, result = self.consume_song_list(directory) + folder, 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) + self.assertEqual(len(result), 0) self.assertEqual(song_list, formated) + def test_tag_cache_suports_sub_directories(self): + track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') + formated = translator.track_to_mpd_format(track, key=True) + result = translator.tracks_to_tag_cache_format([track]) + + result = self.consume_headers(result) + + folder, 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) + 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) + self.assertEqual(len(result), 0) + self.assertEqual(song_list, formated) + + class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/' From 93eda1c81ed8278b58cc27ade626c21068e83178 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 23:46:40 +0200 Subject: [PATCH 064/235] Extra test for multiple top level files --- tests/frontends/mpd/serializer_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 6025109e..40a790c6 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -218,6 +218,24 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.assertEqual(len(result), 0) self.assertEqual(song_list, formated) + def test_tag_cache_supports_multiple_tracks(self): + tracks = [ + Track(uri='file:///dir/subdir/song1.mp3'), + Track(uri='file:///dir/subdir/song2.mp3'), + ] + + formated = [] + formated.extend(translator.track_to_mpd_format(tracks[0], key=True)) + formated.extend(translator.track_to_mpd_format(tracks[1], key=True)) + + result = translator.tracks_to_tag_cache_format(tracks) + + result = self.consume_headers(result) + song_list, result = self.consume_song_list(result) + + self.assertEqual(song_list, formated) + self.assertEqual(len(result), 0) + class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): From 7559426c5069f368281ce489a6bf06155d1f93bf Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Oct 2010 23:56:01 +0200 Subject: [PATCH 065/235] Add test for files in multiple directories --- tests/frontends/mpd/serializer_test.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 40a790c6..6a0dec3a 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -236,6 +236,29 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.assertEqual(song_list, formated) self.assertEqual(len(result), 0) + def test_tag_cache_supports_multiple_tracks_in_dirs(self): + tracks = [ + Track(uri='file:///dir/subdir/song1.mp3'), + Track(uri='file:///dir/subdir/folder/song2.mp3'), + ] + + formated = [] + formated.append(translator.track_to_mpd_format(tracks[0], key=True)) + formated.append(translator.track_to_mpd_format(tracks[1], key=True)) + + result = translator.tracks_to_tag_cache_format(tracks) + + result = self.consume_headers(result) + folder, result = self.consume_directory(result) + song_list, song_result = self.consume_song_list(folder) + + self.assertEqual(song_list, formated[1]) + self.assertEqual(len(song_result), 0) + + song_list, result = self.consume_song_list(result) + self.assertEqual(len(result), 0) + self.assertEqual(song_list, formated[0]) + class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): From ca95a510c90669d2e7ccca865f75acb072d4a7a0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 00:37:42 +0200 Subject: [PATCH 066/235] Ensure that mtime is included --- mopidy/frontends/mpd/translator.py | 6 ++-- tests/frontends/mpd/serializer_test.py | 41 ++++++++++++++++++++------ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 6c93e04e..5d446be8 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -5,6 +5,8 @@ from mopidy import settings from mopidy.frontends.mpd import protocol from mopidy.utils.path import path_to_uri, uri_to_path, split_path +stat = os.stat + def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False): """ Format track for output to MPD client. @@ -40,7 +42,7 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) if key and track.uri: result.insert(0, ('key', os.path.basename(track.uri))) if mtime and track.uri: - mtime = os.stat(uri_to_path(track.uri)).st_mtime + mtime = stat(uri_to_path(track.uri)).st_mtime result.append(('mtime', int(mtime))) return result @@ -127,7 +129,7 @@ def _add_to_tag_cache(result, folders, files): result.append(('songList begin',)) for track in files: - result.extend(track_to_mpd_format(track, key=True)) + result.extend(track_to_mpd_format(track, key=True, mtime=True)) result.append(('songList end',)) def tracks_to_directory_tree(tracks): diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 6a0dec3a..81ea4cae 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -1,4 +1,5 @@ import datetime as dt +import os import unittest from mopidy import settings @@ -7,12 +8,21 @@ from mopidy.models import Album, Artist, Playlist, Track from tests import data_folder, SkipTest +def fake_stat(path): + class StatResult(object): + def __getattr__(self, key): + assert key == 'st_mtime', key + return 1234567 + return StatResult() + class TrackMpdFormatTest(unittest.TestCase): def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' + translator.stat = fake_stat def tearDown(self): settings.runtime.clear() + translator.stat = os.stat def test_track_to_mpd_format_for_empty_track(self): result = translator.track_to_mpd_format(Track()) @@ -46,8 +56,7 @@ class TrackMpdFormatTest(unittest.TestCase): def test_track_to_mpd_format_with_mtime(self): uri = translator.path_to_uri(data_folder('blank.mp3')) result = translator.track_to_mpd_format(Track(uri=uri), mtime=True) - print result - self.assert_(('mtime', 1288125516) in result) + self.assert_(('mtime', 1234567) in result) def test_track_to_mpd_format_track_uses_uri_to_mpd_relative_path(self): track = Track(uri='file:///dir/subdir/song.mp3') @@ -117,11 +126,14 @@ class UriToMpdRelativePathTest(unittest.TestCase): class TracksToTagCacheFormatTest(unittest.TestCase): + def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' + translator.stat = fake_stat def tearDown(self): settings.runtime.clear() + translator.stat = os.stat def consume_headers(self, result): self.assertEqual(('info_begin',), result[0]) @@ -173,7 +185,18 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_has_formated_track(self): track = Track(uri='file:///dir/subdir/song.mp3') - formated = translator.track_to_mpd_format(track, key=True) + formated = translator.track_to_mpd_format(track, key=True, mtime=True) + result = translator.tracks_to_tag_cache_format([track]) + + result = self.consume_headers(result) + song_list, result = self.consume_song_list(result) + + self.assertEqual(song_list, formated) + self.assertEqual(len(result), 0) + + def test_tag_cache_has_formated_track_with_key_and_mtime(self): + track = Track(uri='file:///dir/subdir/song.mp3') + formated = translator.track_to_mpd_format(track, key=True, mtime=True) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -184,7 +207,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_suports_directories(self): track = Track(uri='file:///dir/subdir/folder/song.mp3') - formated = translator.track_to_mpd_format(track, key=True) + formated = translator.track_to_mpd_format(track, key=True, mtime=True) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -199,7 +222,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_suports_sub_directories(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') - formated = translator.track_to_mpd_format(track, key=True) + formated = translator.track_to_mpd_format(track, key=True, mtime=True) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -225,8 +248,8 @@ class TracksToTagCacheFormatTest(unittest.TestCase): ] formated = [] - formated.extend(translator.track_to_mpd_format(tracks[0], key=True)) - formated.extend(translator.track_to_mpd_format(tracks[1], key=True)) + formated.extend(translator.track_to_mpd_format(tracks[0], key=True, mtime=True)) + formated.extend(translator.track_to_mpd_format(tracks[1], key=True, mtime=True)) result = translator.tracks_to_tag_cache_format(tracks) @@ -243,8 +266,8 @@ class TracksToTagCacheFormatTest(unittest.TestCase): ] formated = [] - formated.append(translator.track_to_mpd_format(tracks[0], key=True)) - formated.append(translator.track_to_mpd_format(tracks[1], key=True)) + formated.append(translator.track_to_mpd_format(tracks[0], key=True, mtime=True)) + formated.append(translator.track_to_mpd_format(tracks[1], key=True, mtime=True)) result = translator.tracks_to_tag_cache_format(tracks) From 96d4633306deb7c7bd67fb6844cba6d22337eaeb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 00:40:01 +0200 Subject: [PATCH 067/235] Ensure that key does not have uri encoded strings --- mopidy/frontends/mpd/translator.py | 2 +- tests/frontends/mpd/serializer_test.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 5d446be8..1e2f061a 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -40,7 +40,7 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) result.append(('Pos', position)) result.append(('Id', cpid)) if key and track.uri: - result.insert(0, ('key', os.path.basename(track.uri))) + result.insert(0, ('key', os.path.basename(uri_to_path(track.uri)))) if mtime and track.uri: mtime = stat(uri_to_path(track.uri)).st_mtime result.append(('mtime', int(mtime))) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 81ea4cae..6a993c2a 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -53,6 +53,11 @@ class TrackMpdFormatTest(unittest.TestCase): result = translator.track_to_mpd_format(track, key=True) self.assert_(('key', 'file.mp3') in result) + def test_track_to_mpd_format_with_key_not_uri_encoded(self): + track = Track(uri='file:///dir/subdir/file%20test.mp3') + result = translator.track_to_mpd_format(track, key=True) + self.assert_(('key', 'file test.mp3') in result) + def test_track_to_mpd_format_with_mtime(self): uri = translator.path_to_uri(data_folder('blank.mp3')) result = translator.track_to_mpd_format(Track(uri=uri), mtime=True) From d2d8e4c0906ad95f4ce62279032bdcea93c6c97f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 00:45:08 +0200 Subject: [PATCH 068/235] Use expanduser for find_files --- mopidy/utils/path.py | 1 + tests/utils/path_test.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index ef331045..84869196 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -46,6 +46,7 @@ def split_path(path): return parts def find_files(path): + path = os.path.expanduser(path) if os.path.isfile(path): yield os.path.abspath(path) else: diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index 2990c6d3..269c7a24 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -132,3 +132,6 @@ class FindFilesTest(unittest.TestCase): files = self.find('blank.mp3') self.assertEqual(len(files), 1) self.assert_(files[0], data_folder('blank.mp3')) + + def test_expanduser(self): + raise SkipTest From ea74f539ba4ac6a71c20e7b7591e7f8b9a9e364a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 01:06:26 +0200 Subject: [PATCH 069/235] Fix mistakes in tag cache generation --- mopidy/frontends/mpd/translator.py | 17 +++++++++------ tests/frontends/mpd/serializer_test.py | 30 +++++++++++++++++++------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 1e2f061a..46c3ccbe 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -122,7 +122,10 @@ def tracks_to_tag_cache_format(tracks): return result def _add_to_tag_cache(result, folders, files): - for name, entry in folders.items(): + for path, entry in folders.items(): + name = os.path.split(path)[1] + result.append(('directory', path)) + result.append(('mtime', stat(name).st_mtime)) result.append(('begin', name)) _add_to_tag_cache(result, *entry) result.append(('end', name)) @@ -136,11 +139,13 @@ def tracks_to_directory_tree(tracks): directories = ({}, []) for track in tracks: uri = uri_to_mpd_relative_path(track.uri) - folder = os.path.dirname(uri_to_path(uri)) + path = '' current = directories - for part in split_path(folder): - if part not in current[0]: - current[0][part] = ({}, []) - current = current[0][part] + for part in split_path(os.path.dirname(uri_to_path(uri))): + path = os.path.join(path, part) + print path + if path not in current[0]: + current[0][path] = ({}, []) + current = current[0][path] current[1].append(track) return directories diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 6a993c2a..31b03a7a 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -8,7 +8,7 @@ from mopidy.models import Album, Artist, Playlist, Track from tests import data_folder, SkipTest -def fake_stat(path): +def fake_mtime(path): class StatResult(object): def __getattr__(self, key): assert key == 'st_mtime', key @@ -18,7 +18,7 @@ def fake_stat(path): class TrackMpdFormatTest(unittest.TestCase): def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' - translator.stat = fake_stat + translator.stat = fake_mtime def tearDown(self): settings.runtime.clear() @@ -134,7 +134,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' - translator.stat = fake_stat + translator.stat = fake_mtime def tearDown(self): settings.runtime.clear() @@ -155,11 +155,13 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.fail("Couldn't find songList end in result") def consume_directory(self, result): - self.assertEqual('begin', result[0][0]) - directory = result[0][1] + self.assertEqual('directory', result[0][0]) + self.assertEqual(('mtime', fake_mtime('').st_mtime), result[1]) + self.assertEqual(('begin', os.path.split(result[0][1])[1]), result[2]) + directory = result[2][1] for i, row in enumerate(result): if row == ('end', directory): - return result[1:i], result[i+1:] + return result[3:i], result[i+1:] self.fail("Couldn't find end %s in result" % directory) def test_empty_tag_cache_has_header(self): @@ -225,6 +227,18 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.assertEqual(len(result), 0) self.assertEqual(song_list, formated) + def test_tag_cache_diretory_header_is_right(self): + track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') + formated = translator.track_to_mpd_format(track, key=True, mtime=True) + result = translator.tracks_to_tag_cache_format([track]) + + result = self.consume_headers(result) + folder, result = self.consume_directory(result) + + self.assertEqual(('directory', 'folder/sub'), folder[0]) + self.assertEqual(('mtime', fake_mtime('').st_mtime), folder[1]) + self.assertEqual(('begin', 'sub'), folder[2]) + def test_tag_cache_suports_sub_directories(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') formated = translator.track_to_mpd_format(track, key=True, mtime=True) @@ -317,7 +331,7 @@ class TracksToDirectoryTreeTest(unittest.TestCase): def test_single_file_in_sub_subdir(self): tracks = [Track(uri='file:///dir1/dir2/file1.mp3')] tree = translator.tracks_to_directory_tree(tracks) - expected = ({'dir1': ({'dir2': ({}, tracks)}, [])}, []) + expected = ({'dir1': ({'dir1/dir2': ({}, tracks)}, [])}, []) self.assertEqual(tree, expected) def test_complex_file_structure(self): @@ -334,7 +348,7 @@ class TracksToDirectoryTreeTest(unittest.TestCase): 'dir1': ({}, [tracks[1], tracks[2]]), 'dir2': ( { - 'sub': ({}, [tracks[4]]) + 'dir2/sub': ({}, [tracks[4]]) }, [tracks[3]] ), From 52ab538fc4d0a12fccdb3d7c223fd53b221f0f4f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 20:38:20 +0200 Subject: [PATCH 070/235] Minor test cleanup --- tests/scanner_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index b33ed99a..f0f22ecc 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -51,7 +51,7 @@ class TranslatorTest(unittest.TestCase): def check(self): expected = self.build_track() actual = translator(self.data) - self.assertEqual(expected, translator(self.data)) + self.assertEqual(expected, actual) def test_basic_data(self): self.check() From 8a4dc1033b9d648b89fee951be483738c0f7dbfa Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 20:38:33 +0200 Subject: [PATCH 071/235] Add album-artist support to translator --- mopidy/scanner.py | 7 +++++++ tests/scanner_test.py | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index ccdab24b..c32115f7 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -14,6 +14,7 @@ from mopidy.utils.path import path_to_uri, find_files from mopidy.models import Track, Artist, Album def translator(data): + albumartist_kwargs = {} album_kwargs = {} artist_kwargs = {} track_kwargs = {} @@ -38,6 +39,12 @@ def translator(data): if 'track-number' in data: track_kwargs['track_no'] = data['track-number'] + if 'album-artist' in data: + albumartist_kwargs['name'] = data['album-artist'] + + if albumartist_kwargs: + album_kwargs['artists'] = [Artist(**albumartist_kwargs)] + track_kwargs['uri'] = data['uri'] track_kwargs['length'] = data['duration'] track_kwargs['album'] = Album(**album_kwargs) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index f0f22ecc..141f2ceb 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -19,6 +19,7 @@ class TranslatorTest(unittest.TestCase): 'album': u'albumname', 'track-number': 1, 'artist': u'name', + 'album-artist': 'albumartistname', 'title': u'trackname', 'track-count': 2, 'date': FakeGstDate(2006, 1, 1,), @@ -35,6 +36,10 @@ class TranslatorTest(unittest.TestCase): 'name': 'name', } + self.albumartist = { + 'name': 'albumartistname', + } + self.track = { 'uri': 'uri', 'name': 'trackname', @@ -44,6 +49,8 @@ class TranslatorTest(unittest.TestCase): } def build_track(self): + if self.albumartist: + self.album['artists'] = [Artist(**self.albumartist)] self.track['album'] = Album(**self.album) self.track['artists'] = [Artist(**self.artist)] return Track(**self.track) @@ -81,6 +88,11 @@ class TranslatorTest(unittest.TestCase): del self.artist['name'] self.check() + def test_missing_album_artist(self): + del self.data['album-artist'] + del self.albumartist['name'] + self.check() + def test_missing_date(self): del self.data['date'] del self.track['date'] From 6049c7a0944301c824b01862c4e99a80178acf65 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 20:41:45 +0200 Subject: [PATCH 072/235] Turn track_artists_to_mpd_format into artists_to_mpd_format --- mopidy/frontends/mpd/translator.py | 9 ++++----- tests/frontends/mpd/serializer_test.py | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 46c3ccbe..5f064cc9 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -26,7 +26,7 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) result = [ ('file', uri_to_mpd_relative_path(track.uri) or ''), ('Time', track.length and (track.length // 1000) or 0), - ('Artist', track_artists_to_mpd_format(track)), + ('Artist', artists_to_mpd_format(track.artists)), ('Title', track.name or ''), ('Album', track.album and track.album.name or ''), ('Date', track.date or ''), @@ -46,15 +46,14 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) result.append(('mtime', int(mtime))) return result -def track_artists_to_mpd_format(track): +def artists_to_mpd_format(artists): """ Format track artists for output to MPD client. - :param track: the track - :type track: :class:`mopidy.models.Track` + :param artists: the artists + :type track: array of :class:`mopidy.models.Artist` :rtype: string """ - artists = track.artists artists.sort(key=lambda a: a.name) return u', '.join([a.name for a in artists]) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 31b03a7a..deca960f 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -91,9 +91,9 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Id', 122) in result) self.assertEqual(len(result), 9) - def test_track_artists_to_mpd_format(self): - track = Track(artists=[Artist(name=u'ABBA'), Artist(name=u'Beatles')]) - translated = translator.track_artists_to_mpd_format(track) + def test_artists_to_mpd_format(self): + artists = [Artist(name=u'ABBA'), Artist(name=u'Beatles')] + translated = translator.artists_to_mpd_format(artists) self.assertEqual(translated, u'ABBA, Beatles') From e6cdb8888127057953e842048e28c61eb9d448ac Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 20:42:34 +0200 Subject: [PATCH 073/235] Remove print statement --- mopidy/frontends/mpd/translator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 5f064cc9..da6c6462 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -142,7 +142,6 @@ def tracks_to_directory_tree(tracks): current = directories for part in split_path(os.path.dirname(uri_to_path(uri))): path = os.path.join(path, part) - print path if path not in current[0]: current[0][path] = ({}, []) current = current[0][path] From 9a99bc46bf220e32adbc7c132bb6802d8ccc91f8 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 20:46:47 +0200 Subject: [PATCH 074/235] Add album artist to track to mpd format --- mopidy/frontends/mpd/translator.py | 3 +++ tests/frontends/mpd/serializer_test.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index da6c6462..7b337b62 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -36,6 +36,9 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) track.track_no, track.album.num_tracks))) else: result.append(('Track', track.track_no)) + if track.album is not None and track.album.artists: + artists = artists_to_mpd_format(track.album.artists) + result.append(('AlbumArtist', artists)) if position is not None and cpid is not None: result.append(('Pos', position)) result.append(('Id', cpid)) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index deca960f..5b7e6095 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -74,7 +74,8 @@ class TrackMpdFormatTest(unittest.TestCase): uri=u'a uri', artists=[Artist(name=u'an artist')], name=u'a name', - album=Album(name=u'an album', num_tracks=13), + album=Album(name=u'an album', num_tracks=13, + artists=[Artist(name=u'an other artist')]), track_no=7, date=dt.date(1977, 1, 1), length=137000, @@ -85,11 +86,12 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Artist', 'an artist') in result) self.assert_(('Title', 'a name') in result) self.assert_(('Album', 'an album') in result) + self.assert_(('AlbumArtist', 'an other artist') in result) self.assert_(('Track', '7/13') in result) self.assert_(('Date', dt.date(1977, 1, 1)) in result) self.assert_(('Pos', 9) in result) self.assert_(('Id', 122) in result) - self.assertEqual(len(result), 9) + self.assertEqual(len(result), 10) def test_artists_to_mpd_format(self): artists = [Artist(name=u'ABBA'), Artist(name=u'Beatles')] From 92333208de04857a98e8735b3aecb5034546a335 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 20:53:23 +0200 Subject: [PATCH 075/235] Add mopidy.utils.path.mtime helper that is easily faked in tests --- mopidy/utils/path.py | 17 +++++++++++++++++ tests/utils/path_test.py | 15 ++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 84869196..6bd84ac0 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -54,3 +54,20 @@ def find_files(path): for filename in filenames: dirpath = os.path.abspath(dirpath) yield os.path.join(dirpath, filename) + +class Mtime(object): + def __init__(self): + self.fake = None + + def __call__(self, path): + if self.fake is not None: + return self.fake + return int(os.stat(path).st_mtime) + + def set_fake_time(self, time): + self.fake = time + + def undo_fake(self): + self.fake = None + +mtime = Mtime() diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index 269c7a24..065cde5d 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -6,7 +6,7 @@ import sys import tempfile import unittest -from mopidy.utils.path import (get_or_create_folder, +from mopidy.utils.path import (get_or_create_folder, mtime, path_to_uri, uri_to_path, split_path, find_files) from tests import SkipTest, data_folder @@ -135,3 +135,16 @@ class FindFilesTest(unittest.TestCase): def test_expanduser(self): raise SkipTest + + +class MtimeTest(unittest.TestCase): + def tearDown(self): + mtime.undo_fake() + + def test_mtime_of_current_dir(self): + mtime_dir = int(os.stat('.').st_mtime) + self.assertEqual(mtime_dir, mtime('.')) + + def test_fake_time_is_returned(self): + mtime.set_fake_time(123456) + self.assertEqual(mtime('.'), 123456) From d67bfb9aeeee44d994d24616053fd58a9e6a82fe Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 20:58:41 +0200 Subject: [PATCH 076/235] Use new mtime helper --- mopidy/frontends/mpd/translator.py | 8 +++----- tests/frontends/mpd/serializer_test.py | 20 +++++++------------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 7b337b62..514e167f 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -2,11 +2,10 @@ import os import re from mopidy import settings +from mopidy.utils.path import mtime as get_mtime from mopidy.frontends.mpd import protocol from mopidy.utils.path import path_to_uri, uri_to_path, split_path -stat = os.stat - def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False): """ Format track for output to MPD client. @@ -45,8 +44,7 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) if key and track.uri: result.insert(0, ('key', os.path.basename(uri_to_path(track.uri)))) if mtime and track.uri: - mtime = stat(uri_to_path(track.uri)).st_mtime - result.append(('mtime', int(mtime))) + result.append(('mtime', get_mtime(uri_to_path(track.uri)))) return result def artists_to_mpd_format(artists): @@ -127,7 +125,7 @@ def _add_to_tag_cache(result, folders, files): for path, entry in folders.items(): name = os.path.split(path)[1] result.append(('directory', path)) - result.append(('mtime', stat(name).st_mtime)) + result.append(('mtime', get_mtime(name))) result.append(('begin', name)) _add_to_tag_cache(result, *entry) result.append(('end', name)) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 5b7e6095..74a4184f 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -3,26 +3,20 @@ import os import unittest from mopidy import settings +from mopidy.utils.path import mtime from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, Playlist, Track from tests import data_folder, SkipTest -def fake_mtime(path): - class StatResult(object): - def __getattr__(self, key): - assert key == 'st_mtime', key - return 1234567 - return StatResult() - class TrackMpdFormatTest(unittest.TestCase): def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' - translator.stat = fake_mtime + mtime.set_fake_time(1234567) def tearDown(self): settings.runtime.clear() - translator.stat = os.stat + mtime.undo_fake() def test_track_to_mpd_format_for_empty_track(self): result = translator.track_to_mpd_format(Track()) @@ -136,11 +130,11 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' - translator.stat = fake_mtime + mtime.set_fake_time(1234567) def tearDown(self): settings.runtime.clear() - translator.stat = os.stat + mtime.undo_fake() def consume_headers(self, result): self.assertEqual(('info_begin',), result[0]) @@ -158,7 +152,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def consume_directory(self, result): self.assertEqual('directory', result[0][0]) - self.assertEqual(('mtime', fake_mtime('').st_mtime), result[1]) + self.assertEqual(('mtime', mtime('.')), result[1]) self.assertEqual(('begin', os.path.split(result[0][1])[1]), result[2]) directory = result[2][1] for i, row in enumerate(result): @@ -238,7 +232,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): folder, result = self.consume_directory(result) self.assertEqual(('directory', 'folder/sub'), folder[0]) - self.assertEqual(('mtime', fake_mtime('').st_mtime), folder[1]) + self.assertEqual(('mtime', mtime('.')), folder[1]) self.assertEqual(('begin', 'sub'), folder[2]) def test_tag_cache_suports_sub_directories(self): From b9976c4cdabefe3bf18954c986bad74579ee7ed0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 21:26:08 +0200 Subject: [PATCH 077/235] Remove uri_to_mpd_relative_path --- mopidy/frontends/mpd/translator.py | 17 ++-------- tests/frontends/mpd/serializer_test.py | 47 ++++++-------------------- 2 files changed, 13 insertions(+), 51 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 514e167f..69be17ed 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -23,7 +23,7 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) :rtype: list of two-tuples """ result = [ - ('file', uri_to_mpd_relative_path(track.uri) or ''), + ('file', track.uri or ''), ('Time', track.length and (track.length // 1000) or 0), ('Artist', artists_to_mpd_format(track.artists)), ('Title', track.name or ''), @@ -91,19 +91,6 @@ def playlist_to_mpd_format(playlist, *args, **kwargs): """ return tracks_to_mpd_format(playlist.tracks, *args, **kwargs) -def uri_to_mpd_relative_path(uri): - """ - Strip uri and LOCAL_MUSIC_FOLDER part of uri. - - :param uri: the uri - :type uri: string - :rtype: string - """ - if uri is None: - return '' - path = path_to_uri(settings.LOCAL_MUSIC_FOLDER) - return re.sub('^' + re.escape(path), '', uri) - def tracks_to_tag_cache_format(tracks): """ Format list of tracks for output to MPD tag cache @@ -138,7 +125,7 @@ def _add_to_tag_cache(result, folders, files): def tracks_to_directory_tree(tracks): directories = ({}, []) for track in tracks: - uri = uri_to_mpd_relative_path(track.uri) + uri = track.uri path = '' current = directories for part in split_path(os.path.dirname(uri_to_path(uri))): diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 74a4184f..4123e87f 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -57,12 +57,6 @@ class TrackMpdFormatTest(unittest.TestCase): result = translator.track_to_mpd_format(Track(uri=uri), mtime=True) self.assert_(('mtime', 1234567) in result) - def test_track_to_mpd_format_track_uses_uri_to_mpd_relative_path(self): - track = Track(uri='file:///dir/subdir/song.mp3') - path = dict(translator.track_to_mpd_format(track))['file'] - correct_path = translator.uri_to_mpd_relative_path(track.uri) - self.assertEqual(path, correct_path) - def test_track_to_mpd_format_for_nonempty_track(self): track = Track( uri=u'a uri', @@ -108,26 +102,7 @@ class PlaylistMpdFormatTest(unittest.TestCase): self.assertEqual(dict(result[0])['Track'], 2) -class UriToMpdRelativePathTest(unittest.TestCase): - def setUp(self): - settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' - - def tearDown(self): - settings.runtime.clear() - - def test_none_file_returns_empty_string(self): - uri = 'file:///dir/subdir/music/album/song.mp3' - result = translator.uri_to_mpd_relative_path(None) - self.assertEqual('', result) - - def test_file_gets_stripped(self): - uri = 'file:///dir/subdir/music/album/song.mp3' - result = translator.uri_to_mpd_relative_path(uri) - self.assertEqual('/music/album/song.mp3', result) - - class TracksToTagCacheFormatTest(unittest.TestCase): - def setUp(self): settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' mtime.set_fake_time(1234567) @@ -300,7 +275,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): - settings.LOCAL_MUSIC_FOLDER = '/' + settings.LOCAL_MUSIC_FOLDER = '/root/' def tearDown(self): settings.runtime.clear() @@ -311,32 +286,32 @@ class TracksToDirectoryTreeTest(unittest.TestCase): def test_top_level_files(self): tracks = [ - Track(uri='file:///file1.mp3'), - Track(uri='file:///file2.mp3'), - Track(uri='file:///file3.mp3'), + Track(uri='file:///root/file1.mp3'), + Track(uri='file:///root/file2.mp3'), + Track(uri='file:///root/file3.mp3'), ] tree = translator.tracks_to_directory_tree(tracks) self.assertEqual(tree, ({}, tracks)) def test_single_file_in_subdir(self): - tracks = [Track(uri='file:///dir/file1.mp3')] + tracks = [Track(uri='file:///root/dir/file1.mp3')] tree = translator.tracks_to_directory_tree(tracks) expected = ({'dir': ({}, tracks)}, []) self.assertEqual(tree, expected) def test_single_file_in_sub_subdir(self): - tracks = [Track(uri='file:///dir1/dir2/file1.mp3')] + tracks = [Track(uri='file:///root/dir1/dir2/file1.mp3')] tree = translator.tracks_to_directory_tree(tracks) expected = ({'dir1': ({'dir1/dir2': ({}, tracks)}, [])}, []) self.assertEqual(tree, expected) def test_complex_file_structure(self): tracks = [ - Track(uri='file:///file1.mp3'), - Track(uri='file:///dir1/file2.mp3'), - Track(uri='file:///dir1/file3.mp3'), - Track(uri='file:///dir2/file4.mp3'), - Track(uri='file:///dir2/sub/file5.mp3'), + Track(uri='file:///root/file1.mp3'), + Track(uri='file:///root/dir1/file2.mp3'), + Track(uri='file:///root/dir1/file3.mp3'), + Track(uri='file:///root/dir2/file4.mp3'), + Track(uri='file:///root/dir2/sub/file5.mp3'), ] tree = translator.tracks_to_directory_tree(tracks) expected = ( From f0619744ed3198ac796a12c0acc8da874bcb5d48 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 21:26:53 +0200 Subject: [PATCH 078/235] Fix tracks to direcotory dir handling --- mopidy/frontends/mpd/translator.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 69be17ed..e234046b 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -125,10 +125,15 @@ def _add_to_tag_cache(result, folders, files): def tracks_to_directory_tree(tracks): directories = ({}, []) for track in tracks: - uri = track.uri path = '' current = directories - for part in split_path(os.path.dirname(uri_to_path(uri))): + + local_folder = os.path.expanduser(settings.LOCAL_MUSIC_FOLDER) + track_path = uri_to_path(track.uri) + track_path = re.sub('^' + re.escape(local_folder), '', track_path) + track_dir = os.path.dirname(track_path) + + for part in split_path(track_dir): path = os.path.join(path, part) if path not in current[0]: current[0][path] = ({}, []) From 9309b5bd7d69190ad1f4281c82596bf5e04ad885 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 21:35:31 +0200 Subject: [PATCH 079/235] Handle folders correctly _add_to_tag_cache --- mopidy/frontends/mpd/translator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index e234046b..cf39ec1a 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -111,8 +111,10 @@ def tracks_to_tag_cache_format(tracks): def _add_to_tag_cache(result, folders, files): for path, entry in folders.items(): name = os.path.split(path)[1] + music_folder = os.path.expanduser(settings.LOCAL_MUSIC_FOLDER) + mtime = get_mtime(os.path.join(music_folder, path)) result.append(('directory', path)) - result.append(('mtime', get_mtime(name))) + result.append(('mtime', mtime)) result.append(('begin', name)) _add_to_tag_cache(result, *entry) result.append(('end', name)) From 357591e97e8a4113629e9d070267b79d5cf4130b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 21:53:59 +0200 Subject: [PATCH 080/235] Introduce concept of mpd ordered track info for simpler diffing of tag caches --- mopidy/frontends/mpd/translator.py | 13 ++++++++++++- tests/frontends/mpd/serializer_test.py | 22 +++++++++++++--------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index cf39ec1a..b139967c 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -47,6 +47,14 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) result.append(('mtime', get_mtime(uri_to_path(track.uri)))) return result +MPD_KEY_ORDER = ''' + key file Time Artist AlbumArtist Title Album Track Date MUSICBRAINZ_ALBUMID + MUSICBRAINZ_ALBUMARTISTID MUSICBRAINZ_ARTISTID MUSICBRAINZ_TRACKID mtime +'''.split() + +def order_mpd_track_info(result): + return sorted(result, key=lambda i: MPD_KEY_ORDER.index(i[0])) + def artists_to_mpd_format(artists): """ Format track artists for output to MPD client. @@ -105,6 +113,7 @@ def tracks_to_tag_cache_format(tracks): ('fs_charset', protocol.ENCODING), ('info_end',) ] + tracks.sort(key=lambda t: t.uri) _add_to_tag_cache(result, *tracks_to_directory_tree(tracks)) return result @@ -121,7 +130,9 @@ def _add_to_tag_cache(result, folders, files): result.append(('songList begin',)) for track in files: - result.extend(track_to_mpd_format(track, key=True, mtime=True)) + track_result = track_to_mpd_format(track, key=True, mtime=True) + track_result = order_mpd_track_info(track_result) + result.extend(track_result) result.append(('songList end',)) def tracks_to_directory_tree(tracks): diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 4123e87f..8e8a5d21 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -111,6 +111,10 @@ class TracksToTagCacheFormatTest(unittest.TestCase): settings.runtime.clear() mtime.undo_fake() + def translate(self, track): + result = translator.track_to_mpd_format(track, key=True, mtime=True) + return translator.order_mpd_track_info(result) + def consume_headers(self, result): self.assertEqual(('info_begin',), result[0]) self.assertEqual(('mpd_version', protocol.VERSION), result[1]) @@ -163,7 +167,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_has_formated_track(self): track = Track(uri='file:///dir/subdir/song.mp3') - formated = translator.track_to_mpd_format(track, key=True, mtime=True) + formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -174,7 +178,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 = translator.track_to_mpd_format(track, key=True, mtime=True) + formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -185,7 +189,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_suports_directories(self): track = Track(uri='file:///dir/subdir/folder/song.mp3') - formated = translator.track_to_mpd_format(track, key=True, mtime=True) + formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -200,7 +204,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_diretory_header_is_right(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') - formated = translator.track_to_mpd_format(track, key=True, mtime=True) + formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -212,7 +216,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_suports_sub_directories(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') - formated = translator.track_to_mpd_format(track, key=True, mtime=True) + formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) @@ -238,8 +242,8 @@ class TracksToTagCacheFormatTest(unittest.TestCase): ] formated = [] - formated.extend(translator.track_to_mpd_format(tracks[0], key=True, mtime=True)) - formated.extend(translator.track_to_mpd_format(tracks[1], key=True, mtime=True)) + formated.extend(self.translate(tracks[0])) + formated.extend(self.translate(tracks[1])) result = translator.tracks_to_tag_cache_format(tracks) @@ -256,8 +260,8 @@ class TracksToTagCacheFormatTest(unittest.TestCase): ] formated = [] - formated.append(translator.track_to_mpd_format(tracks[0], key=True, mtime=True)) - formated.append(translator.track_to_mpd_format(tracks[1], key=True, mtime=True)) + formated.append(self.translate(tracks[0])) + formated.append(self.translate(tracks[1])) result = translator.tracks_to_tag_cache_format(tracks) From 52c61634f5e1b2782940eb9b41d4223b15859ac2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 21:57:33 +0200 Subject: [PATCH 081/235] Add doc that explains why on earth order_mpd_track_info is used --- mopidy/frontends/mpd/translator.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index b139967c..96edae49 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -53,6 +53,15 @@ MPD_KEY_ORDER = ''' '''.split() def order_mpd_track_info(result): + """ + Order results from :func:`mopidy.frontends.mpd.translator.track_to_mpd_format` + so that it matches MPD's ordering. Simply a cosmetic fix for easier + diffing of tag_caches. + + :param result: the track info + :type result: list of tuples + :rtype: list of tuples + """ return sorted(result, key=lambda i: MPD_KEY_ORDER.index(i[0])) def artists_to_mpd_format(artists): From 68a78e1c539f126d33ae36fe74d6317470b80bc2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 21:59:28 +0200 Subject: [PATCH 082/235] Add simple proof of concept tag_cache generator --- bin/mopidy-scan | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100755 bin/mopidy-scan diff --git a/bin/mopidy-scan b/bin/mopidy-scan new file mode 100755 index 00000000..73af29fb --- /dev/null +++ b/bin/mopidy-scan @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +if __name__ == '__main__': + import sys + + from mopidy import settings + from mopidy.scanner import Scanner, translator + from mopidy.frontends.mpd.translator import tracks_to_tag_cache_format + + tracks = [] + + def store(data): + track = translator(data) + tracks.append(track) + print >> sys.stderr, 'Added %s' % track.uri + + def debug(uri, error): + print >> sys.stderr, 'Failed %s: %s' % (uri, error) + + print >> sys.stderr, 'Scanning %s' % settings.LOCAL_MUSIC_FOLDER + + scanner = Scanner(settings.LOCAL_MUSIC_FOLDER, store, debug) + scanner.start() + + print >> sys.stderr, 'Done' + + for a in tracks_to_tag_cache_format(tracks): + if len(a) == 1: + print a[0] + else: + print u': '.join([unicode(b) for b in a]) From b8f67c5bb58f10ffa493bdadfe780516ca2a6b29 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 22:18:53 +0200 Subject: [PATCH 083/235] Update docs with respect to mopidy-scan --- docs/settings.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/settings.rst b/docs/settings.rst index afdd39dc..8082b1bb 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -31,6 +31,13 @@ file:: BACKENDS = (u'mopidy.backends.local.LocalBackend',) +Previously this backend relied purely on ``tag_cache`` files from MPD, to +remedy this the command ``mopidy-scan`` has been added. This program will scan +your current ``LOCAL_MUSIC_FOLDER`` and build a MPD compatible ``tag_cache``. +Currently the command outputs the ``tag_cache`` to ``stdout``, this means that +you will need to run ``mopidy-scan > path/to/your/tag_cache`` to actually start +using your new cache. + You may also want to change some of the ``LOCAL_*`` settings. See :mod:`mopidy.settings`, for a full list of available settings. From b7ebedffddcf551848d3d651c266a176dbcd48cd Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 22:19:04 +0200 Subject: [PATCH 084/235] Add mopidy scan to setup.py bin entry --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fabc8353..d0044005 100644 --- a/setup.py +++ b/setup.py @@ -78,7 +78,7 @@ setup( package_data={'mopidy': ['backends/libspotify/spotify_appkey.key']}, cmdclass=cmdclasses, data_files=data_files, - scripts=['bin/mopidy'], + scripts=['bin/mopidy', 'bin/mopidy-scan'], url='http://www.mopidy.com/', license='Apache License, Version 2.0', description='MPD server with Spotify support', From a078a7448e31d4e65ca8755fa31e15869c71f1de Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 22:33:36 +0200 Subject: [PATCH 085/235] Match what mpd expects with regards to caps for encoding --- mopidy/frontends/mpd/protocol/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py index 756aa3c3..6689f627 100644 --- a/mopidy/frontends/mpd/protocol/__init__.py +++ b/mopidy/frontends/mpd/protocol/__init__.py @@ -13,7 +13,7 @@ implement our own MPD server which is compatible with the numerous existing import re #: The MPD protocol uses UTF-8 for encoding all data. -ENCODING = u'utf-8' +ENCODING = u'UTF-8' #: The MPD protocol uses ``\n`` as line terminator. LINE_TERMINATOR = u'\n' From dd259d079703e6d8bfba160c16b1b88debeb970e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 23:12:32 +0200 Subject: [PATCH 086/235] Ensure that find_files only returns unicode --- mopidy/utils/path.py | 6 ++++-- tests/utils/path_test.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 6bd84ac0..78377891 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -48,12 +48,14 @@ def split_path(path): def find_files(path): path = os.path.expanduser(path) if os.path.isfile(path): - yield os.path.abspath(path) + filename = os.path.abspath(path) + yield filename.decode('utf-8') else: for dirpath, dirnames, filenames in os.walk(path): for filename in filenames: dirpath = os.path.abspath(dirpath) - yield os.path.join(dirpath, filename) + filename = os.path.join(dirpath, filename) + yield filename.decode('utf-8') class Mtime(object): def __init__(self): diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index 065cde5d..758a09ab 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -133,6 +133,12 @@ class FindFilesTest(unittest.TestCase): self.assertEqual(len(files), 1) self.assert_(files[0], data_folder('blank.mp3')) + def test_names_are_unicode(self): + is_unicode = lambda f: isinstance(f, unicode) + for name in self.find(''): + self.assert_(is_unicode(name), + '%s is not unicode object' % repr(name)) + def test_expanduser(self): raise SkipTest From f9e49fc5eb09ee22305c9294ae6cc7b0a8d6807c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 30 Oct 2010 23:17:04 +0200 Subject: [PATCH 087/235] Add mopidy.desktop file to get Mopidy into Gnome menus --- MANIFEST.in | 2 +- data/mopidy.desktop | 10 ++++++++++ docs/changes.rst | 3 ++- setup.py | 2 ++ 4 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 data/mopidy.desktop diff --git a/MANIFEST.in b/MANIFEST.in index 38819adb..33d7dc71 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include LICENSE pylintrc *.rst *.txt +include LICENSE pylintrc *.rst *.txt data/mopidy.desktop include mopidy/backends/libspotify/spotify_appkey.key recursive-include docs * prune docs/_build diff --git a/data/mopidy.desktop b/data/mopidy.desktop new file mode 100644 index 00000000..f5ca43bb --- /dev/null +++ b/data/mopidy.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=Mopidy Music Server +Comment=MPD music server with Spotify support +Icon=audio-x-generic +TryExec=mopidy +Exec=mopidy +Terminal=true +Categories=AudioVideo;Audio;Player;ConsoleOnly diff --git a/docs/changes.rst b/docs/changes.rst index cb34993e..aef0055c 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,7 +12,8 @@ No description yet. **Changes** -- None so far. +- Install ``mopidy.desktop`` file that makes Mopidy available from e.g. Gnome + application menus. 0.2.0 (2010-10-24) diff --git a/setup.py b/setup.py index fabc8353..246b41ca 100644 --- a/setup.py +++ b/setup.py @@ -69,6 +69,8 @@ for dirpath, dirnames, filenames in os.walk(project_dir): data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]]) +data_files.append(('/usr/local/share/applications', ['data/mopidy.desktop'])) + setup( name='Mopidy', version=get_version(), From 7767dd1ae4567780dd60b15c18b3b3f443987e03 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 30 Oct 2010 23:35:04 +0200 Subject: [PATCH 088/235] Better unicode handling for scan code --- mopidy/frontends/mpd/translator.py | 2 +- mopidy/scanner.py | 2 +- mopidy/utils/path.py | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 96edae49..2b1adf50 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -147,7 +147,7 @@ def _add_to_tag_cache(result, folders, files): def tracks_to_directory_tree(tracks): directories = ({}, []) for track in tracks: - path = '' + path = u'' current = directories local_folder = os.path.expanduser(settings.LOCAL_MUSIC_FOLDER) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index c32115f7..436598bd 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -83,7 +83,7 @@ class Scanner(object): def process_tags(self, bus, message): data = message.parse_tag() data = dict([(k, data[k]) for k in data.keys()]) - data['uri'] = self.uribin.get_property('uri') + data['uri'] = unicode(self.uribin.get_property('uri')) data['duration'] = self.get_duration() self.data_callback(data) self.next_uri() diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 78377891..b3669e38 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -49,13 +49,17 @@ def find_files(path): path = os.path.expanduser(path) if os.path.isfile(path): filename = os.path.abspath(path) - yield filename.decode('utf-8') + if not isinstance(filename, unicode): + filename = filename.decode('utf-8') + yield filename else: for dirpath, dirnames, filenames in os.walk(path): for filename in filenames: dirpath = os.path.abspath(dirpath) filename = os.path.join(dirpath, filename) - yield filename.decode('utf-8') + if not isinstance(filename, unicode): + filename = filename.decode('utf-8') + yield filename class Mtime(object): def __init__(self): From 39be6d20332394307486f4fe7946c7e400d2a9b8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 00:48:10 +0200 Subject: [PATCH 089/235] Extract GStreamerMessagesThread as a generic GObjectEventThread --- mopidy/core.py | 8 +++++++- mopidy/outputs/gstreamer.py | 21 +++------------------ mopidy/utils/process.py | 25 +++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index 69760094..0be6b96f 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -7,7 +7,7 @@ from mopidy import get_version, settings, OptionalDependencyError from mopidy.utils import get_class from mopidy.utils.log import setup_logging from mopidy.utils.path import get_or_create_folder, get_or_create_file -from mopidy.utils.process import BaseThread +from mopidy.utils.process import BaseThread, GObjectEventThread from mopidy.utils.settings import list_settings_optparse_callback logger = logging.getLogger('mopidy.core') @@ -47,6 +47,7 @@ class CoreProcess(BaseThread): def setup(self): self.setup_logging() self.setup_settings() + self.gobject_loop = self.setup_gobject_loop(self.core_queue) self.output = self.setup_output(self.core_queue) self.backend = self.setup_backend(self.core_queue, self.output) self.frontends = self.setup_frontends(self.core_queue, self.backend) @@ -61,6 +62,11 @@ class CoreProcess(BaseThread): get_or_create_file('~/.mopidy/settings.py') settings.validate() + def setup_gobject_loop(self, core_queue): + gobject_loop = GObjectEventThread(core_queue) + gobject_loop.start() + return gobject_loop + def setup_output(self, core_queue): output = get_class(settings.OUTPUT)(core_queue) output.start() diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 52bd302d..3b037f62 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -1,6 +1,3 @@ -import gobject -gobject.threads_init() - import pygst pygst.require('0.10') import gst @@ -28,20 +25,14 @@ class GStreamerOutput(BaseOutput): def __init__(self, *args, **kwargs): super(GStreamerOutput, self).__init__(*args, **kwargs) - # Start a helper thread that can run the gobject.MainLoop - self.messages_thread = GStreamerMessagesThread(self.core_queue) - - # Start a helper thread that can process the output_queue self.output_queue = multiprocessing.Queue() self.player_thread = GStreamerPlayerThread(self.core_queue, self.output_queue) def start(self): - self.messages_thread.start() self.player_thread.start() def destroy(self): - self.messages_thread.destroy() self.player_thread.destroy() def process_message(self, message): @@ -91,21 +82,15 @@ class GStreamerOutput(BaseOutput): return self._send_recv({'command': 'set_volume', 'volume': volume}) -class GStreamerMessagesThread(BaseThread): - def __init__(self, core_queue): - super(GStreamerMessagesThread, self).__init__(core_queue) - self.name = u'GStreamerMessagesThread' - - def run_inside_try(self): - gobject.MainLoop().run() - - class GStreamerPlayerThread(BaseThread): """ A process for all work related to GStreamer. The main loop processes events from both Mopidy and GStreamer. + This thread requires :class:`mopidy.utils.process.GObjectEventThread` to be + running too. This is not enforced in any way by the code. + Make sure this subprocess is started by the MainThread in the top-most parent process, and not some other thread. If not, we can get into the problems described at diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py index c34d018c..11dafa8a 100644 --- a/mopidy/utils/process.py +++ b/mopidy/utils/process.py @@ -4,6 +4,9 @@ import multiprocessing.dummy from multiprocessing.reduction import reduce_connection import pickle +import gobject +gobject.threads_init() + from mopidy import SettingsError logger = logging.getLogger('mopidy.utils.process') @@ -84,3 +87,25 @@ class BaseThread(multiprocessing.dummy.Process): self.core_queue.put({'to': 'core', 'command': 'exit', 'status': status, 'reason': reason}) self.destroy() + + +class GObjectEventThread(BaseThread): + """ + A GObject event loop which is shared by all Mopidy components that uses + libraries that need a GObject event loop, like GStreamer and D-Bus. + + Should be started by Mopidy's core and used by + :mod:`mopidy.output.gstreamer`, :mod:`mopidy.frontend.mpris`, etc. + """ + + def __init__(self, core_queue): + super(GObjectEventThread, self).__init__(core_queue) + self.name = u'GObjectEventThread' + self.loop = None + + def run_inside_try(self): + self.loop = gobject.MainLoop().run() + + def destroy(self): + self.loop.quit() + super(GObjectEventThread, self).destroy() From abe77112441bf9cc4eb2abc62b94733c154b63b0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 01:48:46 +0200 Subject: [PATCH 090/235] Encode as utf-8 before printing --- bin/mopidy-scan | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/mopidy-scan b/bin/mopidy-scan index 73af29fb..8534372c 100755 --- a/bin/mopidy-scan +++ b/bin/mopidy-scan @@ -28,4 +28,4 @@ if __name__ == '__main__': if len(a) == 1: print a[0] else: - print u': '.join([unicode(b) for b in a]) + print u': '.join([unicode(b) for b in a]).encode('utf-8') From 4b50e802d25fa0ca01d6dd69b4b66ef8306e8bce Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 01:56:44 +0200 Subject: [PATCH 091/235] Expand ~ in LOCAL_TAG_CACHE and LOCAL_MUSIC_FOLDER before use --- mopidy/backends/local/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 04761e17..efcc3bbd 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -143,11 +143,12 @@ class LocalLibraryController(BaseLibraryController): self.refresh() def refresh(self, uri=None): - tracks = parse_mpd_tag_cache(settings.LOCAL_TAG_CACHE, - settings.LOCAL_MUSIC_FOLDER) + tag_cache = os.path.expanduser(settings.LOCAL_TAG_CACHE) + music_folder = os.path.expanduser(settings.LOCAL_MUSIC_FOLDER) - logger.info('Loading songs in %s from %s', - settings.LOCAL_MUSIC_FOLDER, settings.LOCAL_TAG_CACHE) + tracks = parse_mpd_tag_cache(tag_cache, music_folder) + + logger.info('Loading songs in %s from %s', music_folder, tag_cache) for track in tracks: self._uri_mapping[track.uri] = track From 16d44d5e368807b233f342fea1ebcf4e9f87487a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:00:49 +0200 Subject: [PATCH 092/235] Update changelog and docs wrt. mopidy-scan --- docs/changes.rst | 2 ++ docs/settings.rst | 42 +++++++++++++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index aef0055c..c3df7d85 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -14,6 +14,8 @@ No description yet. - Install ``mopidy.desktop`` file that makes Mopidy available from e.g. Gnome application menus. +- Add :command:`mopidy-scan` command to generate ``tag_cache`` files without + any help from the original MPD server. 0.2.0 (2010-10-24) diff --git a/docs/settings.rst b/docs/settings.rst index 8082b1bb..a7638b4e 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -31,16 +31,44 @@ file:: BACKENDS = (u'mopidy.backends.local.LocalBackend',) -Previously this backend relied purely on ``tag_cache`` files from MPD, to -remedy this the command ``mopidy-scan`` has been added. This program will scan -your current ``LOCAL_MUSIC_FOLDER`` and build a MPD compatible ``tag_cache``. -Currently the command outputs the ``tag_cache`` to ``stdout``, this means that -you will need to run ``mopidy-scan > path/to/your/tag_cache`` to actually start -using your new cache. - You may also want to change some of the ``LOCAL_*`` settings. See :mod:`mopidy.settings`, for a full list of available settings. +.. note:: + + Currently, Mopidy supports using Spotify *or* local storage as a music + source. We're working on using both sources simultaneously, and will + hopefully have support for this in the 0.3 release. + + +Generating a tag cache +---------------------- + +Previously the local storage backend relied purely on ``tag_cache`` files +generated by the original MPD server. To remedy this the command +:command:`mopidy-scan` has been created. The program will scan your current +:attr:`mopidy.settings.LOCAL_MUSIC_FOLDER` 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_FOLDER` points to where your + music is located. Check the current setting by running:: + + mopidy --list-settings + +#. Scan your music library. Currently the command outputs the ``tag_cache`` to + ``stdout``, 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` is set to, or change the setting to + point to where your ``tag_cache`` file is. + +#. Start Mopidy, find the music library in a client, and play some local music! + Connecting from other machines on the network ============================================= From 1b20c75d77944358ef91f7b8db9d2fbb66d87dfc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:16:06 +0200 Subject: [PATCH 093/235] docs: Split backend docs into multiple files --- docs/api/backends/dummy.rst | 7 +++++ docs/api/{backends.rst => backends/index.rst} | 26 ++++--------------- docs/api/backends/libspotify.rst | 7 +++++ docs/api/backends/local.rst | 7 +++++ docs/api/frontends/index.rst | 4 +++ 5 files changed, 30 insertions(+), 21 deletions(-) create mode 100644 docs/api/backends/dummy.rst rename docs/api/{backends.rst => backends/index.rst} (74%) create mode 100644 docs/api/backends/libspotify.rst create mode 100644 docs/api/backends/local.rst diff --git a/docs/api/backends/dummy.rst b/docs/api/backends/dummy.rst new file mode 100644 index 00000000..03b2e6ce --- /dev/null +++ b/docs/api/backends/dummy.rst @@ -0,0 +1,7 @@ +********************************************************* +:mod:`mopidy.backends.dummy` -- Dummy backend for testing +********************************************************* + +.. automodule:: mopidy.backends.dummy + :synopsis: Dummy backend used for testing + :members: diff --git a/docs/api/backends.rst b/docs/api/backends/index.rst similarity index 74% rename from docs/api/backends.rst rename to docs/api/backends/index.rst index f675541a..100f6f0d 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends/index.rst @@ -82,25 +82,9 @@ Manages the music library, e.g. searching for tracks to be added to a playlist. :undoc-members: -:mod:`mopidy.backends.dummy` -- Dummy backend for testing -========================================================= +Backends +======== -.. automodule:: mopidy.backends.dummy - :synopsis: Dummy backend used for testing - :members: - - -:mod:`mopidy.backends.libspotify` -- Libspotify backend -======================================================= - -.. automodule:: mopidy.backends.libspotify - :synopsis: Spotify backend using the libspotify library - :members: - - -:mod:`mopidy.backends.local` -- Local backend -===================================================== - -.. automodule:: mopidy.backends.local - :synopsis: Backend for playing music files on local storage - :members: +* :mod:`mopidy.backends.dummy` +* :mod:`mopidy.backends.libspotify` +* :mod:`mopidy.backends.local` diff --git a/docs/api/backends/libspotify.rst b/docs/api/backends/libspotify.rst new file mode 100644 index 00000000..e7528757 --- /dev/null +++ b/docs/api/backends/libspotify.rst @@ -0,0 +1,7 @@ +******************************************************* +:mod:`mopidy.backends.libspotify` -- Libspotify backend +******************************************************* + +.. automodule:: mopidy.backends.libspotify + :synopsis: Spotify backend using the libspotify library + :members: diff --git a/docs/api/backends/local.rst b/docs/api/backends/local.rst new file mode 100644 index 00000000..892f5a87 --- /dev/null +++ b/docs/api/backends/local.rst @@ -0,0 +1,7 @@ +********************************************* +:mod:`mopidy.backends.local` -- Local backend +********************************************* + +.. automodule:: mopidy.backends.local + :synopsis: Backend for playing music files on local storage + :members: diff --git a/docs/api/frontends/index.rst b/docs/api/frontends/index.rst index 05595418..2ab1df8b 100644 --- a/docs/api/frontends/index.rst +++ b/docs/api/frontends/index.rst @@ -4,6 +4,10 @@ A frontend is responsible for exposing Mopidy for a type of clients. +.. automodule:: mopidy.frontends + :synopsis: Frontend API + :members: + Frontend API ============ From bc4d671c862225036e2c6d36ba99e2898fbe3076 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:21:18 +0200 Subject: [PATCH 094/235] docs: Improved description of the frontend concept --- docs/api/frontends/index.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/api/frontends/index.rst b/docs/api/frontends/index.rst index 2ab1df8b..b01bac3d 100644 --- a/docs/api/frontends/index.rst +++ b/docs/api/frontends/index.rst @@ -2,7 +2,12 @@ :mod:`mopidy.frontends` *********************** -A frontend is responsible for exposing Mopidy for a type of clients. +A frontend may do whatever it wants to, including creating threads, opening TCP +ports and exposing Mopidy for a type of clients. + +Frontends got one main limitation: they are restricted to passing messages +through the ``core_queue`` for all communication with the rest of Mopidy. Thus, +the frontend API is very small and reveals little of what a frontend may do. .. automodule:: mopidy.frontends :synopsis: Frontend API From f42b4095bd057b31327693bd1362d300c3462bda Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:25:44 +0200 Subject: [PATCH 095/235] docs: Put changes above install guide. Existing user's should read the changes first --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 7a4dc27d..f53373dc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,11 +6,11 @@ User documentation .. toctree:: :maxdepth: 3 + changes installation/index settings running clients/index - changes authors licenses From f6f0608dd0e06dd1871116d1771a382967abbfd1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:36:04 +0200 Subject: [PATCH 096/235] docs: How to install develop snapshot using pip --- docs/installation/index.rst | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 9577c383..580ecd6d 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -68,11 +68,11 @@ To install the currently latest release of Mopidy using ``pip``:: sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian sudo brew install pip # On OS X - sudo pip install Mopidy + sudo pip install mopidy To later upgrade to the latest release:: - sudo pip install -U Mopidy + sudo pip install -U mopidy If you for some reason can't use ``pip``, try ``easy_install``. @@ -80,26 +80,38 @@ Next, you need to set a couple of :doc:`settings `, and then you're ready to :doc:`run Mopidy `. -Install development version -=========================== +Install development snapshot +============================ -If you want to follow Mopidy development closer, you may install the -development version of Mopidy:: +If you want to follow Mopidy development closer, you may install a snapshot of +Mopidy's ``develop`` branch:: + + sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian + sudo brew install pip # On OS X + sudo pip install mopidy==dev + +Next, you need to set a couple of :doc:`settings `, and then you're +ready to :doc:`run Mopidy `. + + +Run from source code checkout +============================= + +If you may want to contribute to Mopidy, and want access to other branches as +well, you can checkout the Mopidy source from Git and run it directly from the +ckeckout:: sudo aptitude install git-core # On Ubuntu/Debian sudo brew install git # On OS X git clone git://github.com/jodal/mopidy.git cd mopidy/ - sudo python setup.py install + python mopidy # Yes, 'mopidy' is a dir To later update to the very latest version:: cd mopidy/ git pull - sudo python setup.py install For an introduction to ``git``, please visit `git-scm.com -`_. - -Next, you need to set a couple of :doc:`settings `, and then you're -ready to :doc:`run Mopidy `. +`_. Also, please read our :doc:`developer documentation +`. From 1a2f09211dd3d2a484cf64d7c6364bb4e7f2bcda Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:50:38 +0200 Subject: [PATCH 097/235] Move pip requirement files into own dir --- MANIFEST.in | 3 ++- requirements-docs.txt => requirements/docs.txt | 0 .../external_mixers.rst | 0 requirements-lastfm.txt => requirements/lastfm.txt | 0 requirements-tests.txt => requirements/tests.txt | 0 5 files changed, 2 insertions(+), 1 deletion(-) rename requirements-docs.txt => requirements/docs.txt (100%) rename requirements-external-mixers.txt => requirements/external_mixers.rst (100%) rename requirements-lastfm.txt => requirements/lastfm.txt (100%) rename requirements-tests.txt => requirements/tests.txt (100%) diff --git a/MANIFEST.in b/MANIFEST.in index 33d7dc71..f629bcc7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ -include LICENSE pylintrc *.rst *.txt data/mopidy.desktop +include LICENSE pylintrc *.rst data/mopidy.desktop include mopidy/backends/libspotify/spotify_appkey.key recursive-include docs * prune docs/_build +recursive-include requirements * recursive-include tests *.py recursive-include tests/data * diff --git a/requirements-docs.txt b/requirements/docs.txt similarity index 100% rename from requirements-docs.txt rename to requirements/docs.txt diff --git a/requirements-external-mixers.txt b/requirements/external_mixers.rst similarity index 100% rename from requirements-external-mixers.txt rename to requirements/external_mixers.rst diff --git a/requirements-lastfm.txt b/requirements/lastfm.txt similarity index 100% rename from requirements-lastfm.txt rename to requirements/lastfm.txt diff --git a/requirements-tests.txt b/requirements/tests.txt similarity index 100% rename from requirements-tests.txt rename to requirements/tests.txt From 5dc35eaad41330b4a6117138816ca55cd5188897 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:50:53 +0200 Subject: [PATCH 098/235] Fix broken symlinks --- tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 | 2 +- tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 index 45812ac5..e84bdc24 120000 --- a/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 +++ b/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 @@ -1 +1 @@ -../../sample.mp3 \ No newline at end of file +../../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 index 45812ac5..e84bdc24 120000 --- a/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 +++ b/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 @@ -1 +1 @@ -../../sample.mp3 \ No newline at end of file +../../../sample.mp3 \ No newline at end of file From d7dc91f2103def3e77cab0a243749b14e24e1f6e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:51:58 +0200 Subject: [PATCH 099/235] Fix wrong file extension --- requirements/{external_mixers.rst => external_mixers.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename requirements/{external_mixers.rst => external_mixers.txt} (100%) diff --git a/requirements/external_mixers.rst b/requirements/external_mixers.txt similarity index 100% rename from requirements/external_mixers.rst rename to requirements/external_mixers.txt From f50b2b15b1d7c9319bd1d0843a81d3e8e589e587 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:57:04 +0200 Subject: [PATCH 100/235] Add README to requirements/ dir --- requirements/README.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 requirements/README.rst diff --git a/requirements/README.rst b/requirements/README.rst new file mode 100644 index 00000000..cc061a7b --- /dev/null +++ b/requirements/README.rst @@ -0,0 +1,11 @@ +********************* +pip requirement files +********************* + +The files found here are `requirement files +`_ that may be used with `pip +`_. + +To install the dependencies found in one of these files, simply run e.g.:: + + pip install -r requirements/tests.txt From 1437ba63ded573d6ddbeeaf468fe76c5ec7e8f7a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:16:40 +0100 Subject: [PATCH 101/235] docs: Split API and implementation docs --- docs/api/{backends/index.rst => backends.rst} | 4 +- .../{frontends/index.rst => frontends.rst} | 4 +- docs/api/index.rst | 6 +- docs/api/mixers.rst | 65 +++---------------- docs/api/outputs.rst | 10 +-- docs/index.rst | 1 + docs/{api => modules}/backends/dummy.rst | 0 docs/{api => modules}/backends/libspotify.rst | 0 docs/{api => modules}/backends/local.rst | 0 docs/{api => modules}/frontends/lastfm.rst | 0 docs/{api => modules}/frontends/mpd.rst | 0 docs/modules/index.rst | 8 +++ docs/modules/mixers/alsa.rst | 9 +++ docs/modules/mixers/denon.rst | 9 +++ docs/modules/mixers/dummy.rst | 9 +++ docs/modules/mixers/gstreamer_software.rst | 9 +++ docs/modules/mixers/nad.rst | 9 +++ docs/modules/mixers/osa.rst | 9 +++ docs/modules/outputs/gstreamer.rst | 9 +++ 19 files changed, 90 insertions(+), 71 deletions(-) rename docs/api/{backends/index.rst => backends.rst} (97%) rename docs/api/{frontends/index.rst => frontends.rst} (94%) rename docs/{api => modules}/backends/dummy.rst (100%) rename docs/{api => modules}/backends/libspotify.rst (100%) rename docs/{api => modules}/backends/local.rst (100%) rename docs/{api => modules}/frontends/lastfm.rst (100%) rename docs/{api => modules}/frontends/mpd.rst (100%) create mode 100644 docs/modules/index.rst create mode 100644 docs/modules/mixers/alsa.rst create mode 100644 docs/modules/mixers/denon.rst create mode 100644 docs/modules/mixers/dummy.rst create mode 100644 docs/modules/mixers/gstreamer_software.rst create mode 100644 docs/modules/mixers/nad.rst create mode 100644 docs/modules/mixers/osa.rst create mode 100644 docs/modules/outputs/gstreamer.rst diff --git a/docs/api/backends/index.rst b/docs/api/backends.rst similarity index 97% rename from docs/api/backends/index.rst rename to docs/api/backends.rst index 100f6f0d..c8a72b4d 100644 --- a/docs/api/backends/index.rst +++ b/docs/api/backends.rst @@ -82,8 +82,8 @@ Manages the music library, e.g. searching for tracks to be added to a playlist. :undoc-members: -Backends -======== +Backend implementations +======================= * :mod:`mopidy.backends.dummy` * :mod:`mopidy.backends.libspotify` diff --git a/docs/api/frontends/index.rst b/docs/api/frontends.rst similarity index 94% rename from docs/api/frontends/index.rst rename to docs/api/frontends.rst index b01bac3d..2f20c72a 100644 --- a/docs/api/frontends/index.rst +++ b/docs/api/frontends.rst @@ -27,8 +27,8 @@ Frontend API :members: -Frontends -========= +Frontend implementations +======================== * :mod:`mopidy.frontends.lastfm` * :mod:`mopidy.frontends.mpd` diff --git a/docs/api/index.rst b/docs/api/index.rst index 86f4e06e..87ec9bb3 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -1,6 +1,6 @@ -***************** -API documentation -***************** +************* +API reference +************* .. toctree:: :glob: diff --git a/docs/api/mixers.rst b/docs/api/mixers.rst index edaea306..1d9937ac 100644 --- a/docs/api/mixers.rst +++ b/docs/api/mixers.rst @@ -37,61 +37,12 @@ methods as described below. :undoc-members: -:mod:`mopidy.mixers.alsa` -- ALSA mixer for Linux -================================================= +Mixer implementations +===================== -.. inheritance-diagram:: mopidy.mixers.alsa - -.. automodule:: mopidy.mixers.alsa - :synopsis: ALSA mixer for Linux - :members: - - -:mod:`mopidy.mixers.denon` -- Hardware mixer for Denon amplifiers -================================================================= - -.. inheritance-diagram:: mopidy.mixers.denon - -.. automodule:: mopidy.mixers.denon - :synopsis: Hardware mixer for Denon amplifiers - :members: - - -:mod:`mopidy.mixers.dummy` -- Dummy mixer for testing -===================================================== - -.. inheritance-diagram:: mopidy.mixers.dummy - -.. automodule:: mopidy.mixers.dummy - :synopsis: Dummy mixer for testing - :members: - - -:mod:`mopidy.mixers.gstreamer_software` -- Software mixer for all platforms -=========================================================================== - -.. inheritance-diagram:: mopidy.mixers.gstreamer_software - -.. automodule:: mopidy.mixers.gstreamer_software - :synopsis: Software mixer for all platforms - :members: - - -:mod:`mopidy.mixers.osa` -- Osa mixer for OS X -============================================== - -.. inheritance-diagram:: mopidy.mixers.osa - -.. automodule:: mopidy.mixers.osa - :synopsis: Osa mixer for OS X - :members: - - -:mod:`mopidy.mixers.nad` -- Hardware mixer for NAD amplifiers -============================================================= - -.. inheritance-diagram:: mopidy.mixers.nad - -.. automodule:: mopidy.mixers.nad - :synopsis: Hardware mixer for NAD amplifiers - :members: +* :mod:`mopidy.mixers.alsa` +* :mod:`mopidy.mixers.denon` +* :mod:`mopidy.mixers.dummy` +* :mod:`mopidy.mixers.gstreamer_software` +* :mod:`mopidy.mixers.osa` +* :mod:`mopidy.mixers.nad` diff --git a/docs/api/outputs.rst b/docs/api/outputs.rst index 8f4e33c0..d8c2932e 100644 --- a/docs/api/outputs.rst +++ b/docs/api/outputs.rst @@ -12,11 +12,7 @@ A stable output API is not available yet, as we've only implemented a single output module. -:mod:`mopidy.outputs.gstreamer` -- GStreamer output for all platforms -===================================================================== +Output implementations +====================== -.. inheritance-diagram:: mopidy.outputs.gstreamer - -.. automodule:: mopidy.outputs.gstreamer - :synopsis: GStreamer output for all platforms - :members: +* :mod:`mopidy.outputs.gstreamer` diff --git a/docs/index.rst b/docs/index.rst index f53373dc..09029a4f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,6 +21,7 @@ Reference documentation :maxdepth: 3 api/index + modules/index Development documentation ========================= diff --git a/docs/api/backends/dummy.rst b/docs/modules/backends/dummy.rst similarity index 100% rename from docs/api/backends/dummy.rst rename to docs/modules/backends/dummy.rst diff --git a/docs/api/backends/libspotify.rst b/docs/modules/backends/libspotify.rst similarity index 100% rename from docs/api/backends/libspotify.rst rename to docs/modules/backends/libspotify.rst diff --git a/docs/api/backends/local.rst b/docs/modules/backends/local.rst similarity index 100% rename from docs/api/backends/local.rst rename to docs/modules/backends/local.rst diff --git a/docs/api/frontends/lastfm.rst b/docs/modules/frontends/lastfm.rst similarity index 100% rename from docs/api/frontends/lastfm.rst rename to docs/modules/frontends/lastfm.rst diff --git a/docs/api/frontends/mpd.rst b/docs/modules/frontends/mpd.rst similarity index 100% rename from docs/api/frontends/mpd.rst rename to docs/modules/frontends/mpd.rst diff --git a/docs/modules/index.rst b/docs/modules/index.rst new file mode 100644 index 00000000..44da0028 --- /dev/null +++ b/docs/modules/index.rst @@ -0,0 +1,8 @@ +**************** +Module reference +**************** + +.. toctree:: + :glob: + + ** diff --git a/docs/modules/mixers/alsa.rst b/docs/modules/mixers/alsa.rst new file mode 100644 index 00000000..05f429eb --- /dev/null +++ b/docs/modules/mixers/alsa.rst @@ -0,0 +1,9 @@ +************************************************* +:mod:`mopidy.mixers.alsa` -- ALSA mixer for Linux +************************************************* + +.. inheritance-diagram:: mopidy.mixers.alsa + +.. automodule:: mopidy.mixers.alsa + :synopsis: ALSA mixer for Linux + :members: diff --git a/docs/modules/mixers/denon.rst b/docs/modules/mixers/denon.rst new file mode 100644 index 00000000..ac944ccc --- /dev/null +++ b/docs/modules/mixers/denon.rst @@ -0,0 +1,9 @@ +***************************************************************** +:mod:`mopidy.mixers.denon` -- Hardware mixer for Denon amplifiers +***************************************************************** + +.. inheritance-diagram:: mopidy.mixers.denon + +.. automodule:: mopidy.mixers.denon + :synopsis: Hardware mixer for Denon amplifiers + :members: diff --git a/docs/modules/mixers/dummy.rst b/docs/modules/mixers/dummy.rst new file mode 100644 index 00000000..6665f949 --- /dev/null +++ b/docs/modules/mixers/dummy.rst @@ -0,0 +1,9 @@ +***************************************************** +:mod:`mopidy.mixers.dummy` -- Dummy mixer for testing +***************************************************** + +.. inheritance-diagram:: mopidy.mixers.dummy + +.. automodule:: mopidy.mixers.dummy + :synopsis: Dummy mixer for testing + :members: diff --git a/docs/modules/mixers/gstreamer_software.rst b/docs/modules/mixers/gstreamer_software.rst new file mode 100644 index 00000000..ef8cc310 --- /dev/null +++ b/docs/modules/mixers/gstreamer_software.rst @@ -0,0 +1,9 @@ +*************************************************************************** +:mod:`mopidy.mixers.gstreamer_software` -- Software mixer for all platforms +*************************************************************************** + +.. inheritance-diagram:: mopidy.mixers.gstreamer_software + +.. automodule:: mopidy.mixers.gstreamer_software + :synopsis: Software mixer for all platforms + :members: diff --git a/docs/modules/mixers/nad.rst b/docs/modules/mixers/nad.rst new file mode 100644 index 00000000..d441b3fd --- /dev/null +++ b/docs/modules/mixers/nad.rst @@ -0,0 +1,9 @@ +************************************************************* +:mod:`mopidy.mixers.nad` -- Hardware mixer for NAD amplifiers +************************************************************* + +.. inheritance-diagram:: mopidy.mixers.nad + +.. automodule:: mopidy.mixers.nad + :synopsis: Hardware mixer for NAD amplifiers + :members: diff --git a/docs/modules/mixers/osa.rst b/docs/modules/mixers/osa.rst new file mode 100644 index 00000000..14bf9a49 --- /dev/null +++ b/docs/modules/mixers/osa.rst @@ -0,0 +1,9 @@ +********************************************** +:mod:`mopidy.mixers.osa` -- Osa mixer for OS X +********************************************** + +.. inheritance-diagram:: mopidy.mixers.osa + +.. automodule:: mopidy.mixers.osa + :synopsis: Osa mixer for OS X + :members: diff --git a/docs/modules/outputs/gstreamer.rst b/docs/modules/outputs/gstreamer.rst new file mode 100644 index 00000000..69c77dad --- /dev/null +++ b/docs/modules/outputs/gstreamer.rst @@ -0,0 +1,9 @@ +********************************************************************* +:mod:`mopidy.outputs.gstreamer` -- GStreamer output for all platforms +********************************************************************* + +.. inheritance-diagram:: mopidy.outputs.gstreamer + +.. automodule:: mopidy.outputs.gstreamer + :synopsis: GStreamer output for all platforms + :members: From 69fbe82b56820b179862762ee3eb4b0caf46766a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 02:28:56 +0100 Subject: [PATCH 102/235] Update Sphinx Makefile with new targets --- docs/Makefile | 130 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 78 insertions(+), 52 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 4ad8691e..6a3272f4 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,101 +4,127 @@ # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -PAPER = +PAPER = +BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: - -rm -rf _build/* + -rm -rf $(BUILDDIR)/* html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo - @echo "Build finished. The HTML pages are in _build/html." + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo - @echo "Build finished. The HTML pages are in _build/dirhtml." + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in _build/htmlhelp." + ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) _build/qthelp + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in _build/qthelp, like this:" - @echo "# qcollectiongenerator _build/qthelp/Mopidy.qhcp" + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Mopidy.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile _build/qthelp/Mopidy.qhc" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Mopidy.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Mopidy" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Mopidy" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo - @echo "Build finished; the LaTeX files are in _build/latex." - @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ - "run these through (pdf)latex." + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo - @echo "The overview file is in _build/changes." + @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ - "or in _build/linkcheck/output.txt." + "or in $(BUILDDIR)/linkcheck/output.txt." doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ - "results in _build/doctest/output.txt." - -public: clean dirhtml - rm -rf /tmp/mopidy-html && cp -r _build/dirhtml /tmp/mopidy-html - git stash save - cd .. && \ - git checkout gh-pages && \ - git pull && \ - rm -r * && \ - cp -r /tmp/mopidy-html/* . && \ - mv _sources sources && \ - (find . -type f | xargs sed -i -e 's/_sources/sources/g') && \ - mv _static static && \ - (find . -type f | xargs sed -i -e 's/_static/static/g') && \ - if [ -d _images ]; then mv _images images; fi && \ - (find . -type f | xargs sed -i -e 's/_images/images/g') && \ - git add * + "results in $(BUILDDIR)/doctest/output.txt." From a34532171e48cc3c4537f55ddaea61da7757ff43 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 03:17:29 +0100 Subject: [PATCH 103/235] docs: Update roadmap --- docs/development/roadmap.rst | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst index 645cbd30..c88ecdf0 100644 --- a/docs/development/roadmap.rst +++ b/docs/development/roadmap.rst @@ -14,11 +14,13 @@ release. Possible targets for the next version ===================================== -- Reintroduce support for OS X. See :issue:`14` for details. -- Support for using multiple Mopidy backends simultaneously. Should make it - possible to have both Spotify tracks and local tracks in the same playlist. +- Reintroduce support for OS X. See :issue:`25` for details. +- **[WIP: feature/multi-backend]** Support for using multiple Mopidy backends + simultaneously. Should make it possible to have both Spotify tracks and local + tracks in the same playlist. - MPD frontend: + - **[WIP: feature/mpd-password]** Password authentication. - ``idle`` support. - Spotify backend: @@ -30,10 +32,10 @@ Possible targets for the next version - Local backend: - Better library support. - - A script for creating a tag cache. + - **[DONE: v0.3]** A script for creating a tag cache. - An alternative to tag cache for caching metadata, i.e. Sqlite. -- **[DONE]** Last.fm scrobbling. +- **[DONE: v0.2]** Last.fm scrobbling. Stuff we want to do, but not right now, and maybe never @@ -52,7 +54,8 @@ Stuff we want to do, but not right now, and maybe never - Compatability: - - Run frontend tests against a real MPD server to ensure we are in sync. + - **[WIP: feature/blackbox-testing]** Run frontend tests against a real MPD + server to ensure we are in sync. - Backends: @@ -64,7 +67,7 @@ Stuff we want to do, but not right now, and maybe never - Publish the server's presence to the network using `Zeroconf `_/Avahi. - - D-Bus/`MPRIS `_ + - **[WIP: feature/mpris-frontend]** D-Bus/`MPRIS `_ - REST/JSON web service with a jQuery client as example application. Maybe based upon `Tornado `_ and `jQuery Mobile `_. From 5442fc246356323792e6effa53e505e1568e579b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 14:22:53 +0100 Subject: [PATCH 104/235] Add Debian packaging support --- debian/TODO | 14 +++++++++++++ debian/changelog | 5 +++++ debian/compat | 1 + debian/control | 22 +++++++++++++++++++++ debian/copyright | 38 ++++++++++++++++++++++++++++++++++++ debian/docs | 2 ++ debian/menu | 2 ++ debian/pyversions | 1 + debian/rules | 27 +++++++++++++++++++++++++ debian/source/format | 1 + debian/watch | 2 ++ docs/development/roadmap.rst | 8 ++++---- 12 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 debian/TODO create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/docs create mode 100644 debian/menu create mode 100644 debian/pyversions create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 debian/watch diff --git a/debian/TODO b/debian/TODO new file mode 100644 index 00000000..51bab1ce --- /dev/null +++ b/debian/TODO @@ -0,0 +1,14 @@ +To do for Mopidy's Debian packaging +=================================== + +- Install data/mopidy.desktop into /usr/share/applications/ +- Add manpages for all commands. Build the manpages with Sphinx +- Make init script run Mopidy as a daemon +- Make init script run Mopidy with it's own user +- Add support for reading settings from /etc/mopidy/settings.py +- Log to /var/log +- Cache files in /var/cache +- Package pyspotify and add it to Recommends +- Package pylast and add it to Recommends +- Create GPG key for signing the package +- Host the packages at PPA or apt.mopidy.com diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 00000000..a04f2e78 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +mopidy (0.2.0-1) unstable; urgency=low + + * Initial release + + -- Stein Magnus Jodal Sun, 31 Oct 2010 13:07:04 +0100 diff --git a/debian/compat b/debian/compat new file mode 100644 index 00000000..7f8f011e --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +7 diff --git a/debian/control b/debian/control new file mode 100644 index 00000000..c2755717 --- /dev/null +++ b/debian/control @@ -0,0 +1,22 @@ +Source: mopidy +Section: sound +Priority: optional +Maintainer: Stein Magnus Jodal +Build-Depends: debhelper (>= 7.0.50~), python-support, python (>= 2.6), + python-sphinx (>= 1.0), python-pygraphviz +Standards-Version: 3.9.1 +Homepage: http://www.mopidy.com/ +Vcs-Git: git://github.com/jodal/mopidy.git +Vcs-Browser: http://github.com/jodal/mopidy + +Package: mopidy +Architecture: all +Depends: ${misc:Depends}, ${python:Depends}, python-gst0.10 +Recommends: gstreamer0.10-plugins-good, gstreamer0.10-plugins-ugly +Suggests: python-alsaaudio (>= 0.2), python-serial +Description: music server with MPD client support + Mopidy is a music server which can play music from Spotify or from your + local hard drive. To search for music in Spotify’s vast archive, manage + playlists, and play music, you can use most MPD clients. MPD clients are + available for most platforms, including Windows, Mac OS X, Linux, and + iPhone and Android phones. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 00000000..c29416d6 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,38 @@ +This work was packaged for Debian by: + + Stein Magnus Jodal on Sun, 31 Oct 2010 09:50:28 +0100 + +It was downloaded from: + + http://pypi.python.org/packages/source/M/Mopidy/Mopidy-0.2.0.tar.gz + +Upstream Author(s): + + Stein Magnus Jodal + +Copyright: + + Copyright 2009-2010 Stein Magnus Jodal and contributors + +License: + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. See the License for the specific language governing + permissions and limitations under the License. + +On Debian systems, the complete text of the Apache version 2.0 license +can be found in "/usr/share/common-licenses/Apache-2.0". + +The Debian packaging is: + + Copyright 2010 Stein Magnus Jodal + +and is licensed under the Apache License, Version 2.0, see above. diff --git a/debian/docs b/debian/docs new file mode 100644 index 00000000..a4b46448 --- /dev/null +++ b/debian/docs @@ -0,0 +1,2 @@ +README.rst +docs/_build/html/ diff --git a/debian/menu b/debian/menu new file mode 100644 index 00000000..6376a81e --- /dev/null +++ b/debian/menu @@ -0,0 +1,2 @@ +?package(mopidy):needs="text" section="Applications/Sound"\ + title="Mopidy" command="/usr/bin/mopidy" diff --git a/debian/pyversions b/debian/pyversions new file mode 100644 index 00000000..0c043f18 --- /dev/null +++ b/debian/pyversions @@ -0,0 +1 @@ +2.6- diff --git a/debian/rules b/debian/rules new file mode 100755 index 00000000..926a81b1 --- /dev/null +++ b/debian/rules @@ -0,0 +1,27 @@ +#!/usr/bin/make -f +# -*- makefile -*- +# Sample debian/rules that uses debhelper. +# This file was originally written by Joey Hess and Craig Small. +# As a special exception, when this file is copied by dh-make into a +# dh-make output file, you may use that output file without restriction. +# This special exception was added by Craig Small in version 0.37 of dh-make. + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +%: + dh $@ + +override_dh_clean: + make -C docs/ clean + dh_clean + +override_dh_installchangelogs: + dh_installchangelogs docs/changes.rst + +override_dh_installdocs: + make -C docs/ clean html + dh_installdocs + +.PHONY: override_dh_clean override_dh_installchangelogs \ + override_dh_installdocs override_dh_installinit diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 00000000..163aaf8d --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/watch b/debian/watch new file mode 100644 index 00000000..3d4d3a41 --- /dev/null +++ b/debian/watch @@ -0,0 +1,2 @@ +version=3 +http://pypi.python.org/packages/source/M/Mopidy/Mopidy-(.*)\.tar\.gz diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst index c88ecdf0..2b0cf2ba 100644 --- a/docs/development/roadmap.rst +++ b/docs/development/roadmap.rst @@ -47,10 +47,10 @@ Stuff we want to do, but not right now, and maybe never recipies for all our dependencies and Mopidy itself to make OS X installation a breeze. See `Homebrew's issue #1612 `_. - - Create `Debian packages `_ of all - our dependencies and Mopidy itself (hosted in our own Debian repo until we - get stuff into the various distros) to make Debian/Ubuntu installation a - breeze. + - **[WIP]** Create `Debian packages + `_ of all our dependencies and + Mopidy itself (hosted in our own Debian repo until we get stuff into the + various distros) to make Debian/Ubuntu installation a breeze. - Compatability: From 15ccb16e5d2ff4dcaa70d41d1721de4b38d4e1fa Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 14:23:52 +0100 Subject: [PATCH 105/235] Fix typo --- debian/TODO | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/TODO b/debian/TODO index 51bab1ce..4551dc55 100644 --- a/debian/TODO +++ b/debian/TODO @@ -4,7 +4,7 @@ To do for Mopidy's Debian packaging - Install data/mopidy.desktop into /usr/share/applications/ - Add manpages for all commands. Build the manpages with Sphinx - Make init script run Mopidy as a daemon -- Make init script run Mopidy with it's own user +- Make init script run Mopidy with its own user - Add support for reading settings from /etc/mopidy/settings.py - Log to /var/log - Cache files in /var/cache From 545ad9486cbcd486ae2227b8e0355ccb6080f206 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 15:56:22 +0100 Subject: [PATCH 106/235] docs: Add local paths to the start and not end of PYTHONPATH --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index d0d8f3af..16a85975 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,8 +16,8 @@ import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.append(os.path.abspath(os.path.dirname(__file__))) -sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/../')) +sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) +sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/../')) import mopidy From acda478ed3c0781301b3502c9a84b68c7b999d13 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 23:23:40 +0100 Subject: [PATCH 107/235] docs: Start splitting controllers into controllers and providers --- docs/api/backends.rst | 62 +++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/docs/api/backends.rst b/docs/api/backends.rst index c8a72b4d..92334ab8 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends.rst @@ -6,29 +6,43 @@ :synopsis: Backend API -The backend and its controllers -=============================== +The backend, controller, and provider concepts +============================================== -.. graph:: backend_relations +Backend: + The backend is mostly for convenience. It is a container that holds + references to all the controllers. +Controllers: + Each controller has responsibility for a given part of the backend + functionality. Most, but not all, controllers delegates some work to one or + more providers. The controllers are responsible for choosing the right + provider for any given task based upon i.e. the track's URI. +Providers: + Anything specific to i.e. Spotify integration or local storage is contained + in the providers. To integrate with new music sources, you just add new + providers. - backend -- current_playlist - backend -- library - backend -- playback - backend -- stored_playlists +.. digraph:: backend_relations + Backend -> "Current\nplaylist\ncontroller" + Backend -> "Library\ncontroller" + "Library\ncontroller" -> "Library\nproviders" + Backend -> "Playback\ncontroller" + "Playback\ncontroller" -> "Playback\nproviders" + Backend -> "Stored\nplaylists\ncontroller" + "Stored\nplaylists\ncontroller" -> "Stored\nplaylist\nproviders" + Backend -> Mixer + +.. _backend-api: Backend API =========== .. note:: - Currently this only documents the API that is available for use by - frontends like :mod:`mopidy.frontends.mpd`, and not what is required to - implement your own backend. :class:`mopidy.backends.base.BaseBackend` and - its controllers implements many of these methods in a matter that should be - independent of most concrete backend implementations, so you should - generally just implement or override a few of these methods yourself to - create a new backend with a complete feature set. + The backend API is the interface that is used by frontends like + :mod:`mopidy.frontends.mpd`. If you want to implement your own backend, see + the :ref:`provider-api`. .. autoclass:: mopidy.backends.base.BaseBackend :members: @@ -82,6 +96,26 @@ Manages the music library, e.g. searching for tracks to be added to a playlist. :undoc-members: +.. _provider-api: + +Provider API +============ + +.. note:: + + The provider API is the interface that must be implemented when you create + a backend. If you are working on a frontend and need to access the backend, + see the :ref:`backend-api`. + + +Playback provider +----------------- + +.. autoclass:: mopidy.backends.base.BasePlaybackProvider + :members: + :undoc-members: + + Backend implementations ======================= From 91f9180c39b19f329bf23c9fbac44915b878b933 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 1 Nov 2010 00:33:32 +0100 Subject: [PATCH 108/235] Add some basic fixmes --- mopidy/backends/local/translator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 87ea15df..b6d25b5b 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -106,6 +106,7 @@ def _convert_mpd_data(data, tracks, music_dir): track_kwargs['artists'] = [artist] album_kwargs['artists'] = [artist] + # FIXME Newer mpd tag caches support albumartist names if 'album' in data: album_kwargs['name'] = data['album'] album = Album(**album_kwargs) @@ -114,11 +115,15 @@ def _convert_mpd_data(data, tracks, music_dir): if 'title' in data: track_kwargs['name'] = data['title'] + # FIXME what if file is uri - generated tag cache needs to allways make + # LOCAL_MUSIC_PATH relative paths or this code must handle uris if data['file'][0] == '/': path = data['file'][1:] else: path = data['file'] + # FIXME newer mpd tag caches provide musicbrainz ids + track_kwargs['uri'] = path_to_uri(music_dir, path) track_kwargs['length'] = int(data.get('time', 0)) * 1000 From 70fe571b055b9bc44267d06cd9a925197df81955 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 31 Oct 2010 20:58:58 +0100 Subject: [PATCH 109/235] Add special casing of _PATH settings --- mopidy/utils/settings.py | 3 +++ tests/utils/settings_test.py | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index ac75cb70..6286c8b9 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -51,6 +51,9 @@ class SettingsProxy(object): value = self.current[attr] if type(value) != bool and not value: raise SettingsError(u'Setting "%s" is empty.' % attr) + if attr.endswith('_PATH'): + value = os.path.expanduser(value) + value = os.path.abspath(value) return value def __setattr__(self, attr, value): diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index 0c06ae5c..a8b98647 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -1,3 +1,4 @@ +import os import unittest from mopidy import settings as default_settings_module @@ -65,3 +66,15 @@ class SettingsProxyTest(unittest.TestCase): 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' + acctual = self.settings.TEST_PATH + expected = os.path.expanduser('~/test') + self.assertEqual(acctual, expected) + + def test_value_ending_in_path_is_absolute(self): + self.settings.TEST_PATH = './test' + acctual = self.settings.TEST_PATH + expected = os.path.abspath('./test') + self.assertEqual(acctual, expected) From e8371129f74a696b004ac3bd7ad6ee893d7e455d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 31 Oct 2010 23:19:30 +0100 Subject: [PATCH 110/235] Add test to ensure that other values are not expanded --- tests/utils/settings_test.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index a8b98647..3b1d7439 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -73,8 +73,12 @@ class SettingsProxyTest(unittest.TestCase): expected = os.path.expanduser('~/test') self.assertEqual(acctual, expected) - def test_value_ending_in_path_is_absolute(self): - self.settings.TEST_PATH = './test' - acctual = self.settings.TEST_PATH - expected = os.path.abspath('./test') - self.assertEqual(acctual, expected) + def test_value_not_ending_in_path_is_not_expanded(self): + self.settings.TEST = '~/test' + acctual = self.settings.TEST + self.assertEqual(acctual, '~/test') + + def test_value_not_ending_in_path_is_not_absolute(self): + self.settings.TEST = './test' + acctual = self.settings.TEST + self.assertEqual(acctual, './test') From 3661800563ca59463f5b5e36b254ab7aaa207665 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 31 Oct 2010 23:33:04 +0100 Subject: [PATCH 111/235] Add _FILE handling --- mopidy/utils/settings.py | 2 +- tests/utils/settings_test.py | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 6286c8b9..3145d278 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -51,7 +51,7 @@ class SettingsProxy(object): value = self.current[attr] if type(value) != bool and not value: raise SettingsError(u'Setting "%s" is empty.' % attr) - if attr.endswith('_PATH'): + if attr.endswith('_PATH') or attr.endswith('_FILE'): value = os.path.expanduser(value) value = os.path.abspath(value) return value diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index 3b1d7439..cef0069d 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -73,12 +73,30 @@ class SettingsProxyTest(unittest.TestCase): expected = os.path.expanduser('~/test') self.assertEqual(acctual, expected) - def test_value_not_ending_in_path_is_not_expanded(self): + def test_value_ending_in_path_is_absolute(self): + self.settings.TEST_PATH = './test' + acctual = self.settings.TEST_PATH + expected = os.path.abspath('./test') + self.assertEqual(acctual, expected) + + def test_value_ending_in_file_is_expanded(self): + self.settings.TEST_FILE = '~/test' + acctual = self.settings.TEST_FILE + expected = os.path.expanduser('~/test') + self.assertEqual(acctual, expected) + + def test_value_ending_in_file_is_absolute(self): + self.settings.TEST_FILE = './test' + acctual = self.settings.TEST_FILE + expected = os.path.abspath('./test') + self.assertEqual(acctual, expected) + + def test_value_not_ending_in_path_or_file_is_not_expanded(self): self.settings.TEST = '~/test' acctual = self.settings.TEST self.assertEqual(acctual, '~/test') - def test_value_not_ending_in_path_is_not_absolute(self): + def test_value_not_ending_in_path_or_file_is_not_absolute(self): self.settings.TEST = './test' acctual = self.settings.TEST self.assertEqual(acctual, './test') From c513d0f6e7ea053628b240e185a572669b645b27 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 31 Oct 2010 23:44:45 +0100 Subject: [PATCH 112/235] Rename settings that have dirs or file to _PATH and _FILE --- bin/mopidy | 0 bin/mopidy-scan | 4 ++-- docs/settings.rst | 6 +++--- mopidy/backends/libspotify/__init__.py | 2 +- mopidy/backends/libspotify/session_manager.py | 4 ++-- mopidy/backends/local/__init__.py | 12 ++++++------ mopidy/frontends/mpd/translator.py | 4 ++-- mopidy/settings.py | 14 +++++++------- mopidy/utils/settings.py | 4 ++++ tests/backends/base/stored_playlists.py | 10 +++++----- tests/backends/local/library_test.py | 4 ++-- tests/backends/local/stored_playlists_test.py | 12 ++++++------ tests/frontends/mpd/serializer_test.py | 6 +++--- 13 files changed, 43 insertions(+), 39 deletions(-) mode change 100644 => 100755 bin/mopidy diff --git a/bin/mopidy b/bin/mopidy old mode 100644 new mode 100755 diff --git a/bin/mopidy-scan b/bin/mopidy-scan index 8534372c..1865f317 100755 --- a/bin/mopidy-scan +++ b/bin/mopidy-scan @@ -17,9 +17,9 @@ if __name__ == '__main__': def debug(uri, error): print >> sys.stderr, 'Failed %s: %s' % (uri, error) - print >> sys.stderr, 'Scanning %s' % settings.LOCAL_MUSIC_FOLDER + print >> sys.stderr, 'Scanning %s' % settings.LOCAL_MUSIC_PATH - scanner = Scanner(settings.LOCAL_MUSIC_FOLDER, store, debug) + scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug) scanner.start() print >> sys.stderr, 'Done' diff --git a/docs/settings.rst b/docs/settings.rst index a7638b4e..41507a4a 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -47,12 +47,12 @@ Generating a tag cache Previously the local storage backend relied purely on ``tag_cache`` files generated by the original MPD server. To remedy this the command :command:`mopidy-scan` has been created. The program will scan your current -:attr:`mopidy.settings.LOCAL_MUSIC_FOLDER` and build a MPD compatible +:attr:`mopidy.settings.LOCAL_MUSIC_PATH` 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_FOLDER` points to where your +#. Ensure that :attr:`mopidy.settings.LOCAL_MUSIC_PATH` points to where your music is located. Check the current setting by running:: mopidy --list-settings @@ -64,7 +64,7 @@ To make a ``tag_cache`` of your local music available for Mopidy: mopidy-scan > tag_cache #. Move the ``tag_cache`` file to the location - :attr:`mopidy.settings.LOCAL_TAG_CACHE` is set to, or change the setting to + :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE` is set to, or change the setting to point to where your ``tag_cache`` file is. #. Start Mopidy, find the music library in a client, and play some local music! diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index 223d9968..75739d66 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -18,7 +18,7 @@ class LibspotifyBackend(BaseBackend): **Settings:** - - :attr:`mopidy.settings.SPOTIFY_LIB_CACHE` + - :attr:`mopidy.settings.SPOTIFY_CACHE_PATH` - :attr:`mopidy.settings.SPOTIFY_USERNAME` - :attr:`mopidy.settings.SPOTIFY_PASSWORD` diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/libspotify/session_manager.py index 45841350..3e11fc6f 100644 --- a/mopidy/backends/libspotify/session_manager.py +++ b/mopidy/backends/libspotify/session_manager.py @@ -15,8 +15,8 @@ logger = logging.getLogger('mopidy.backends.libspotify.session_manager') # LibspotifySessionManager: Too many ancestors (9/7) class LibspotifySessionManager(SpotifySessionManager, BaseThread): - cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) - settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) + cache_location = os.path.expanduser(settings.SPOTIFY_CACHE_PATH) + settings_location = os.path.expanduser(settings.SPOTIFY_CACHE_PATH) appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key') user_agent = 'Mopidy %s' % get_version() diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index efcc3bbd..01dcf1df 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -23,9 +23,9 @@ class LocalBackend(BaseBackend): **Settings:** - - :attr:`mopidy.settings.LOCAL_MUSIC_FOLDER` - - :attr:`mopidy.settings.LOCAL_PLAYLIST_FOLDER` - - :attr:`mopidy.settings.LOCAL_TAG_CACHE` + - :attr:`mopidy.settings.LOCAL_MUSIC_PATH` + - :attr:`mopidy.settings.LOCAL_PLAYLIST_PATH` + - :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE` """ def __init__(self, *args, **kwargs): @@ -66,7 +66,7 @@ class LocalPlaybackController(BasePlaybackController): class LocalStoredPlaylistsController(BaseStoredPlaylistsController): def __init__(self, *args): super(LocalStoredPlaylistsController, self).__init__(*args) - self._folder = os.path.expanduser(settings.LOCAL_PLAYLIST_FOLDER) + self._folder = os.path.expanduser(settings.LOCAL_PLAYLIST_PATH) self.refresh() def lookup(self, uri): @@ -143,8 +143,8 @@ class LocalLibraryController(BaseLibraryController): self.refresh() def refresh(self, uri=None): - tag_cache = os.path.expanduser(settings.LOCAL_TAG_CACHE) - music_folder = os.path.expanduser(settings.LOCAL_MUSIC_FOLDER) + tag_cache = os.path.expanduser(settings.LOCAL_TAG_CACHE_FILE) + music_folder = os.path.expanduser(settings.LOCAL_MUSIC_PATH) tracks = parse_mpd_tag_cache(tag_cache, music_folder) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 2b1adf50..c7f5605a 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -129,7 +129,7 @@ def tracks_to_tag_cache_format(tracks): def _add_to_tag_cache(result, folders, files): for path, entry in folders.items(): name = os.path.split(path)[1] - music_folder = os.path.expanduser(settings.LOCAL_MUSIC_FOLDER) + music_folder = os.path.expanduser(settings.LOCAL_MUSIC_PATH) mtime = get_mtime(os.path.join(music_folder, path)) result.append(('directory', path)) result.append(('mtime', mtime)) @@ -150,7 +150,7 @@ def tracks_to_directory_tree(tracks): path = u'' current = directories - local_folder = os.path.expanduser(settings.LOCAL_MUSIC_FOLDER) + local_folder = os.path.expanduser(settings.LOCAL_MUSIC_PATH) track_path = uri_to_path(track.uri) track_path = re.sub('^' + re.escape(local_folder), '', track_path) track_dir = os.path.dirname(track_path) diff --git a/mopidy/settings.py b/mopidy/settings.py index c9d7b9fc..4f60ee99 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -77,8 +77,8 @@ LASTFM_PASSWORD = u'' #: #: Default:: #: -#: LOCAL_MUSIC_FOLDER = u'~/music' -LOCAL_MUSIC_FOLDER = u'~/music' +#: LOCAL_MUSIC_PATH = u'~/music' +LOCAL_MUSIC_PATH = u'~/music' #: Path to playlist folder with m3u files for local music. #: @@ -86,8 +86,8 @@ LOCAL_MUSIC_FOLDER = u'~/music' #: #: Default:: #: -#: LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists' -LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists' +#: LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists' +LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists' #: Path to tag cache for local music. #: @@ -95,8 +95,8 @@ LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists' #: #: Default:: #: -#: LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache' -LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache' +#: LOCAL_TAG_CACHE_FILE = u'~/.mopidy/tag_cache' +LOCAL_TAG_CACHE_FILE = u'~/.mopidy/tag_cache' #: Sound mixer to use. See :mod:`mopidy.mixers` for all available mixers. #: @@ -172,7 +172,7 @@ MPD_SERVER_PORT = 6600 #: Path to the libspotify cache. #: #: Used by :mod:`mopidy.backends.libspotify`. -SPOTIFY_LIB_CACHE = u'~/.mopidy/libspotify_cache' +SPOTIFY_CACHE_PATH = u'~/.mopidy/libspotify_cache' #: Your Spotify Premium username. #: diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 3145d278..2ec0f716 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -97,10 +97,14 @@ def validate_settings(defaults, settings): 'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME', 'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT', 'FRONTEND': 'FRONTENDS', + 'LOCAL_MUSIC_FOLDER': 'LOCAL_MUSIC_PATH', + 'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH', + 'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE', 'SERVER': None, 'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME', 'SERVER_PORT': 'MPD_SERVER_PORT', 'SPOTIFY_LIB_APPKEY': None, + 'SPOTIFY_LIB_CACHE': 'SPOTIFY_CACHE_PATH', } for setting, value in settings.iteritems(): diff --git a/tests/backends/base/stored_playlists.py b/tests/backends/base/stored_playlists.py index ef5806ef..5bcd322c 100644 --- a/tests/backends/base/stored_playlists.py +++ b/tests/backends/base/stored_playlists.py @@ -10,9 +10,9 @@ from tests import SkipTest, data_folder class BaseStoredPlaylistsControllerTest(object): def setUp(self): - settings.LOCAL_PLAYLIST_FOLDER = tempfile.mkdtemp() - settings.LOCAL_TAG_CACHE = data_folder('library_tag_cache') - settings.LOCAL_MUSIC_FOLDER = data_folder('') + settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp() + settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache') + settings.LOCAL_MUSIC_PATH = data_folder('') self.backend = self.backend_class(mixer_class=DummyMixer) self.stored = self.backend.stored_playlists @@ -20,8 +20,8 @@ class BaseStoredPlaylistsControllerTest(object): def tearDown(self): self.backend.destroy() - if os.path.exists(settings.LOCAL_PLAYLIST_FOLDER): - shutil.rmtree(settings.LOCAL_PLAYLIST_FOLDER) + if os.path.exists(settings.LOCAL_PLAYLIST_PATH): + shutil.rmtree(settings.LOCAL_PLAYLIST_PATH) settings.runtime.clear() diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index c0605ef2..34465d09 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -17,8 +17,8 @@ class LocalLibraryControllerTest(BaseLibraryControllerTest, unittest.TestCase): backend_class = LocalBackend def setUp(self): - settings.LOCAL_TAG_CACHE = data_folder('library_tag_cache') - settings.LOCAL_MUSIC_FOLDER = data_folder('') + settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache') + settings.LOCAL_MUSIC_PATH = data_folder('') super(LocalLibraryControllerTest, self).setUp() diff --git a/tests/backends/local/stored_playlists_test.py b/tests/backends/local/stored_playlists_test.py index bb03f997..4db9e1e2 100644 --- a/tests/backends/local/stored_playlists_test.py +++ b/tests/backends/local/stored_playlists_test.py @@ -25,13 +25,13 @@ class LocalStoredPlaylistsControllerTest(BaseStoredPlaylistsControllerTest, backend_class = LocalBackend def test_created_playlist_is_persisted(self): - path = os.path.join(settings.LOCAL_PLAYLIST_FOLDER, 'test.m3u') + path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u') self.assert_(not os.path.exists(path)) self.stored.create('test') self.assert_(os.path.exists(path)) def test_saved_playlist_is_persisted(self): - path = os.path.join(settings.LOCAL_PLAYLIST_FOLDER, 'test2.m3u') + path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test2.m3u') self.assert_(not os.path.exists(path)) self.stored.save(Playlist(name='test2')) self.assert_(os.path.exists(path)) @@ -39,13 +39,13 @@ class LocalStoredPlaylistsControllerTest(BaseStoredPlaylistsControllerTest, def test_deleted_playlist_get_removed(self): playlist = self.stored.create('test') self.stored.delete(playlist) - path = os.path.join(settings.LOCAL_PLAYLIST_FOLDER, 'test.m3u') + path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u') self.assert_(not os.path.exists(path)) def test_renamed_playlist_gets_moved(self): playlist = self.stored.create('test') - file1 = os.path.join(settings.LOCAL_PLAYLIST_FOLDER, 'test.m3u') - file2 = os.path.join(settings.LOCAL_PLAYLIST_FOLDER, 'test2.m3u') + file1 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u') + file2 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test2.m3u') self.assert_(not os.path.exists(file2)) self.stored.rename(playlist, 'test2') self.assert_(not os.path.exists(file1)) @@ -55,7 +55,7 @@ class LocalStoredPlaylistsControllerTest(BaseStoredPlaylistsControllerTest, track = Track(uri=generate_song(1)) uri = track.uri[len('file://'):] playlist = Playlist(tracks=[track], name='test') - path = os.path.join(settings.LOCAL_PLAYLIST_FOLDER, 'test.m3u') + path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u') self.stored.save(playlist) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 8e8a5d21..db0dabf4 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -11,7 +11,7 @@ from tests import data_folder, SkipTest class TrackMpdFormatTest(unittest.TestCase): def setUp(self): - settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' + settings.LOCAL_MUSIC_PATH = '/dir/subdir' mtime.set_fake_time(1234567) def tearDown(self): @@ -104,7 +104,7 @@ class PlaylistMpdFormatTest(unittest.TestCase): class TracksToTagCacheFormatTest(unittest.TestCase): def setUp(self): - settings.LOCAL_MUSIC_FOLDER = '/dir/subdir' + settings.LOCAL_MUSIC_PATH = '/dir/subdir' mtime.set_fake_time(1234567) def tearDown(self): @@ -279,7 +279,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): - settings.LOCAL_MUSIC_FOLDER = '/root/' + settings.LOCAL_MUSIC_PATH = '/root/' def tearDown(self): settings.runtime.clear() From fe309d374e0934b481a3b3d87886692f362621fb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 31 Oct 2010 23:51:10 +0100 Subject: [PATCH 113/235] Get rid of all kinds of abspath and expandpath usage that is no longer needed --- mopidy/backends/libspotify/session_manager.py | 4 ++-- mopidy/backends/local/__init__.py | 6 +++--- mopidy/frontends/mpd/translator.py | 4 ++-- mopidy/scanner.py | 1 - mopidy/utils/path.py | 12 +++--------- tests/utils/path_test.py | 6 ------ 6 files changed, 10 insertions(+), 23 deletions(-) diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/libspotify/session_manager.py index 3e11fc6f..5831b713 100644 --- a/mopidy/backends/libspotify/session_manager.py +++ b/mopidy/backends/libspotify/session_manager.py @@ -15,8 +15,8 @@ logger = logging.getLogger('mopidy.backends.libspotify.session_manager') # LibspotifySessionManager: Too many ancestors (9/7) class LibspotifySessionManager(SpotifySessionManager, BaseThread): - cache_location = os.path.expanduser(settings.SPOTIFY_CACHE_PATH) - settings_location = os.path.expanduser(settings.SPOTIFY_CACHE_PATH) + cache_location = settings.SPOTIFY_CACHE_PATH + settings_location = settings.SPOTIFY_CACHE_PATH appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key') user_agent = 'Mopidy %s' % get_version() diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 01dcf1df..21eea945 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -66,7 +66,7 @@ class LocalPlaybackController(BasePlaybackController): class LocalStoredPlaylistsController(BaseStoredPlaylistsController): def __init__(self, *args): super(LocalStoredPlaylistsController, self).__init__(*args) - self._folder = os.path.expanduser(settings.LOCAL_PLAYLIST_PATH) + self._folder = settings.LOCAL_PLAYLIST_PATH self.refresh() def lookup(self, uri): @@ -143,8 +143,8 @@ class LocalLibraryController(BaseLibraryController): self.refresh() def refresh(self, uri=None): - tag_cache = os.path.expanduser(settings.LOCAL_TAG_CACHE_FILE) - music_folder = os.path.expanduser(settings.LOCAL_MUSIC_PATH) + tag_cache = settings.LOCAL_TAG_CACHE_FILE + music_folder = settings.LOCAL_MUSIC_PATH tracks = parse_mpd_tag_cache(tag_cache, music_folder) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index c7f5605a..fc1f031b 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -129,7 +129,7 @@ def tracks_to_tag_cache_format(tracks): def _add_to_tag_cache(result, folders, files): for path, entry in folders.items(): name = os.path.split(path)[1] - music_folder = os.path.expanduser(settings.LOCAL_MUSIC_PATH) + music_folder = settings.LOCAL_MUSIC_PATH mtime = get_mtime(os.path.join(music_folder, path)) result.append(('directory', path)) result.append(('mtime', mtime)) @@ -150,7 +150,7 @@ def tracks_to_directory_tree(tracks): path = u'' current = directories - local_folder = os.path.expanduser(settings.LOCAL_MUSIC_PATH) + local_folder = settings.LOCAL_MUSIC_PATH track_path = uri_to_path(track.uri) track_path = re.sub('^' + re.escape(local_folder), '', track_path) track_dir = os.path.dirname(track_path) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 436598bd..4ccccbdb 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -5,7 +5,6 @@ import pygst pygst.require('0.10') import gst -from os.path import abspath import datetime import sys import threading diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index b3669e38..5476e1fd 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -7,14 +7,12 @@ import urllib logger = logging.getLogger('mopidy.utils.path') def get_or_create_folder(folder): - folder = os.path.expanduser(folder) if not os.path.isdir(folder): logger.info(u'Creating dir %s', folder) os.mkdir(folder, 0755) return folder def get_or_create_file(filename): - filename = os.path.expanduser(filename) if not os.path.isfile(filename): logger.info(u'Creating file %s', filename) open(filename, 'w') @@ -22,7 +20,6 @@ def get_or_create_file(filename): def path_to_uri(*paths): path = os.path.join(*paths) - #path = os.path.expanduser(path) # FIXME Waiting for test case? path = path.encode('utf-8') if sys.platform == 'win32': return 'file:' + urllib.pathname2url(path) @@ -46,16 +43,13 @@ def split_path(path): return parts def find_files(path): - path = os.path.expanduser(path) if os.path.isfile(path): - filename = os.path.abspath(path) - if not isinstance(filename, unicode): - filename = filename.decode('utf-8') - yield filename + if not isinstance(path, unicode): + path = path.decode('utf-8') + yield path else: for dirpath, dirnames, filenames in os.walk(path): for filename in filenames: - dirpath = os.path.abspath(dirpath) filename = os.path.join(dirpath, filename) if not isinstance(filename, unicode): filename = filename.decode('utf-8') diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index 758a09ab..4366305c 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -34,9 +34,6 @@ class GetOrCreateFolderTest(unittest.TestCase): self.assert_(os.path.isdir(self.parent)) self.assertEqual(created, self.parent) - def test_that_userfolder_is_expanded(self): - raise SkipTest # Not sure how to safely test this - class PathToFileURITest(unittest.TestCase): def test_simple_path(self): @@ -139,9 +136,6 @@ class FindFilesTest(unittest.TestCase): self.assert_(is_unicode(name), '%s is not unicode object' % repr(name)) - def test_expanduser(self): - raise SkipTest - class MtimeTest(unittest.TestCase): def tearDown(self): From 71ee3a8ef74db145af731a0b2fd46b9c029e7bc9 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 1 Nov 2010 00:07:59 +0100 Subject: [PATCH 114/235] Don't remove expanduser from get_or_create_file and get_or_create_folder --- mopidy/utils/path.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 5476e1fd..f25d754a 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -7,12 +7,14 @@ import urllib logger = logging.getLogger('mopidy.utils.path') def get_or_create_folder(folder): + folder = os.path.expanduser(folder) if not os.path.isdir(folder): logger.info(u'Creating dir %s', folder) os.mkdir(folder, 0755) return folder def get_or_create_file(filename): + filename = os.path.expanduser(filename) if not os.path.isfile(filename): logger.info(u'Creating file %s', filename) open(filename, 'w') From 913bac3b0d91fc3521a1311101d47311acbeacc4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 1 Nov 2010 00:56:06 +0100 Subject: [PATCH 115/235] Rename with_ to copy --- mopidy/backends/dummy/__init__.py | 2 +- mopidy/backends/local/__init__.py | 2 +- mopidy/models.py | 2 +- tests/models_test.py | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 62cbd7e2..42acdbf2 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -93,7 +93,7 @@ class DummyStoredPlaylistsController(BaseStoredPlaylistsController): def rename(self, playlist, new_name): self._playlists[self._playlists.index(playlist)] = \ - playlist.with_(name=new_name) + playlist.copy(name=new_name) def save(self, playlist): self._playlists.append(playlist) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 21eea945..c8331a48 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -116,7 +116,7 @@ class LocalStoredPlaylistsController(BaseStoredPlaylistsController): src = os.path.join(self._folder, playlist.name + '.m3u') dst = os.path.join(self._folder, name + '.m3u') - renamed = playlist.with_(name=name) + renamed = playlist.copy(name=name) index = self._playlists.index(playlist) self._playlists[index] = renamed diff --git a/mopidy/models.py b/mopidy/models.py index c5877657..e691ccb7 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -179,7 +179,7 @@ class Playlist(ImmutableObject): def mpd_format(self, *args, **kwargs): return translator.playlist_to_mpd_format(self, *args, **kwargs) - def with_(self, uri=None, name=None, tracks=None, last_modified=None): + def copy(self, uri=None, name=None, tracks=None, last_modified=None): """ Create a new playlist object with the given values. The values that are not given are taken from the object the method is called on. diff --git a/tests/models_test.py b/tests/models_test.py index ab7bc793..1ccf16ea 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -398,7 +398,7 @@ class PlaylistTest(unittest.TestCase): last_modified = dt.datetime.now() playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks, last_modified=last_modified) - new_playlist = playlist.with_(uri=u'another uri') + new_playlist = playlist.copy(uri=u'another uri') self.assertEqual(new_playlist.uri, u'another uri') self.assertEqual(new_playlist.name, u'a name') self.assertEqual(new_playlist.tracks, tracks) @@ -409,7 +409,7 @@ class PlaylistTest(unittest.TestCase): last_modified = dt.datetime.now() playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks, last_modified=last_modified) - new_playlist = playlist.with_(name=u'another name') + new_playlist = playlist.copy(name=u'another name') self.assertEqual(new_playlist.uri, u'an uri') self.assertEqual(new_playlist.name, u'another name') self.assertEqual(new_playlist.tracks, tracks) @@ -421,7 +421,7 @@ class PlaylistTest(unittest.TestCase): playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks, last_modified=last_modified) new_tracks = [Track(), Track()] - new_playlist = playlist.with_(tracks=new_tracks) + new_playlist = playlist.copy(tracks=new_tracks) self.assertEqual(new_playlist.uri, u'an uri') self.assertEqual(new_playlist.name, u'a name') self.assertEqual(new_playlist.tracks, new_tracks) @@ -433,7 +433,7 @@ class PlaylistTest(unittest.TestCase): new_last_modified = last_modified + dt.timedelta(1) playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks, last_modified=last_modified) - new_playlist = playlist.with_(last_modified=new_last_modified) + new_playlist = playlist.copy(last_modified=new_last_modified) self.assertEqual(new_playlist.uri, u'an uri') self.assertEqual(new_playlist.name, u'a name') self.assertEqual(new_playlist.tracks, tracks) From a9e53e06172571eb2a2bb6762430e433794d6b39 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Nov 2010 22:00:13 +0100 Subject: [PATCH 116/235] docs: Rename 'backend api' to 'backend controller api', and 'provider api' to 'backend provider api' --- docs/api/backends.rst | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/api/backends.rst b/docs/api/backends.rst index 92334ab8..de1957e8 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends.rst @@ -3,7 +3,7 @@ ********************** .. automodule:: mopidy.backends - :synopsis: Backend API + :synopsis: Backend APIs The backend, controller, and provider concepts @@ -33,16 +33,16 @@ Providers: "Stored\nplaylists\ncontroller" -> "Stored\nplaylist\nproviders" Backend -> Mixer -.. _backend-api: +.. _backend-controller-api: -Backend API -=========== +Backend controller API +====================== .. note:: - The backend API is the interface that is used by frontends like + The backend controller API is the interface that is used by frontends like :mod:`mopidy.frontends.mpd`. If you want to implement your own backend, see - the :ref:`provider-api`. + the :ref:`backend-provider-api`. .. autoclass:: mopidy.backends.base.BaseBackend :members: @@ -96,16 +96,16 @@ Manages the music library, e.g. searching for tracks to be added to a playlist. :undoc-members: -.. _provider-api: +.. _backend-provider-api: -Provider API -============ +Backend provider API +==================== .. note:: - The provider API is the interface that must be implemented when you create - a backend. If you are working on a frontend and need to access the backend, - see the :ref:`backend-api`. + The backend provider API is the interface that must be implemented when you + create a backend. If you are working on a frontend and need to access the + backend, see the :ref:`backend-controller-api`. Playback provider From beeda047858d0287912f2c8cfff76d1e7fbedd7b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Nov 2010 22:52:56 +0100 Subject: [PATCH 117/235] Add destroy() to playback provider API --- mopidy/backends/base/playback.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 0d4ef52f..8b935c65 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -68,10 +68,8 @@ class BasePlaybackController(object): def destroy(self): """ Cleanup after component. - - May be overridden by subclasses. """ - pass + self.provider.destroy() def _get_cpid(self, cp_track): if cp_track is None: @@ -496,6 +494,14 @@ class BasePlaybackProvider(object): def __init__(self, backend): self.backend = backend + def destroy(self): + """ + Cleanup after component. + + May be overridden by subclasses. + """ + pass + def pause(self): """ To be overridden by subclass. Implement your backend's pause From 11bedc46dd5446339217c7c96e428d28c652c7bd Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 1 Nov 2010 23:05:15 +0100 Subject: [PATCH 118/235] Change how mopidy-scan prints tag-cache --- bin/mopidy-scan | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/mopidy-scan b/bin/mopidy-scan index 1865f317..84cfee57 100755 --- a/bin/mopidy-scan +++ b/bin/mopidy-scan @@ -26,6 +26,6 @@ if __name__ == '__main__': for a in tracks_to_tag_cache_format(tracks): if len(a) == 1: - print a[0] + print (u'%s' % a).encode('utf-8') else: - print u': '.join([unicode(b) for b in a]).encode('utf-8') + print (u'%s: %s' % a).encode('utf-8') From d4437d5adcb45df7cc9a32a4ddeafa543a39261f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 1 Nov 2010 23:06:45 +0100 Subject: [PATCH 119/235] Decode all data from tag_cache as utf-8 --- mopidy/backends/local/translator.py | 2 +- tests/backends/local/translator_test.py | 11 ++++++++++- tests/data/utf8_tag_cache | 13 +++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 tests/data/utf8_tag_cache diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index b6d25b5b..26c2ad6b 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -84,7 +84,7 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''): _convert_mpd_data(current, tracks, music_dir) current.clear() - current[key.lower()] = value + current[key.lower()] = value.decode('utf-8') _convert_mpd_data(current, tracks, music_dir) diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py index a9fe58d8..2f97e45c 100644 --- a/tests/backends/local/translator_test.py +++ b/tests/backends/local/translator_test.py @@ -116,7 +116,16 @@ class MPDTagCacheToTracksTest(unittest.TestCase): self.assertEqual(set(expected_tracks), tracks) def test_unicode_cache(self): - raise SkipTest + tracks = parse_mpd_tag_cache(data_folder('utf8_tag_cache'), + data_folder('')) + + uri = path_to_uri(data_folder('song1.mp3')) + artists = [Artist(name=u'æøå')] + album = Album(name=u'æøå', artists=artists) + track = Track(uri=uri, name=u'æøå', artists=artists, + album=album, length=4000) + + self.assertEqual(track, list(tracks)[0]) def test_misencoded_cache(self): # FIXME not sure if this can happen diff --git a/tests/data/utf8_tag_cache b/tests/data/utf8_tag_cache new file mode 100644 index 00000000..6642ec77 --- /dev/null +++ b/tests/data/utf8_tag_cache @@ -0,0 +1,13 @@ +info_begin +mpd_version: 0.14.2 +fs_charset: UTF-8 +info_end +songList begin +key: song1.mp3 +file: /song1.mp3 +Time: 4 +Artist: æøå +Title: æøå +Album: æøå +mtime: 1272319626 +songList end From 4ac1d8d217b4b16ead73b74d838b0e8633370c56 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 1 Nov 2010 23:09:21 +0100 Subject: [PATCH 120/235] Remove mtime and key from track_to_mpd_format --- mopidy/frontends/mpd/translator.py | 6 +----- tests/frontends/mpd/serializer_test.py | 15 --------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index fc1f031b..d470d8e3 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -6,7 +6,7 @@ from mopidy.utils.path import mtime as get_mtime from mopidy.frontends.mpd import protocol from mopidy.utils.path import path_to_uri, uri_to_path, split_path -def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False): +def track_to_mpd_format(track, position=None, cpid=None): """ Format track for output to MPD client. @@ -41,10 +41,6 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False) if position is not None and cpid is not None: result.append(('Pos', position)) result.append(('Id', cpid)) - if key and track.uri: - result.insert(0, ('key', os.path.basename(uri_to_path(track.uri)))) - if mtime and track.uri: - result.append(('mtime', get_mtime(uri_to_path(track.uri)))) return result MPD_KEY_ORDER = ''' diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index db0dabf4..1336dabe 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -42,21 +42,6 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Pos', 1) in result) self.assert_(('Id', 2) in result) - def test_track_to_mpd_format_with_key(self): - track = Track(uri='file:///dir/subdir/file.mp3') - result = translator.track_to_mpd_format(track, key=True) - self.assert_(('key', 'file.mp3') in result) - - def test_track_to_mpd_format_with_key_not_uri_encoded(self): - track = Track(uri='file:///dir/subdir/file%20test.mp3') - result = translator.track_to_mpd_format(track, key=True) - self.assert_(('key', 'file test.mp3') in result) - - def test_track_to_mpd_format_with_mtime(self): - uri = translator.path_to_uri(data_folder('blank.mp3')) - result = translator.track_to_mpd_format(Track(uri=uri), mtime=True) - self.assert_(('mtime', 1234567) in result) - def test_track_to_mpd_format_for_nonempty_track(self): track = Track( uri=u'a uri', From 8bfa7d293770b71338a23c1a571c25c8d295e674 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 1 Nov 2010 23:16:49 +0100 Subject: [PATCH 121/235] Update tag cache code so it adds mtime and file that is relative --- mopidy/frontends/mpd/translator.py | 11 ++++++++--- tests/frontends/mpd/serializer_test.py | 10 +++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index d470d8e3..e5a55960 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -123,9 +123,11 @@ def tracks_to_tag_cache_format(tracks): return result def _add_to_tag_cache(result, folders, files): + music_folder = settings.LOCAL_MUSIC_PATH + regexp = '^' + re.escape(music_folder).rstrip('/') + '/?' + for path, entry in folders.items(): name = os.path.split(path)[1] - music_folder = settings.LOCAL_MUSIC_PATH mtime = get_mtime(os.path.join(music_folder, path)) result.append(('directory', path)) result.append(('mtime', mtime)) @@ -135,8 +137,11 @@ def _add_to_tag_cache(result, folders, files): result.append(('songList begin',)) for track in files: - track_result = track_to_mpd_format(track, key=True, mtime=True) - track_result = order_mpd_track_info(track_result) + track_result = dict(track_to_mpd_format(track)) + path = uri_to_path(track_result['file']) + track_result['mtime'] = get_mtime(path) + track_result['file'] = re.sub(regexp, '', path) + track_result = order_mpd_track_info(track_result.items()) result.extend(track_result) result.append(('songList end',)) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 1336dabe..6fe50657 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -3,7 +3,7 @@ import os import unittest from mopidy import settings -from mopidy.utils.path import mtime +from mopidy.utils.path import mtime, uri_to_path from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, Playlist, Track @@ -97,8 +97,12 @@ class TracksToTagCacheFormatTest(unittest.TestCase): mtime.undo_fake() def translate(self, track): - result = translator.track_to_mpd_format(track, key=True, mtime=True) - return translator.order_mpd_track_info(result) + folder = settings.LOCAL_MUSIC_PATH + result = dict(translator.track_to_mpd_format(track)) + result['file'] = uri_to_path(result['file']) + result['file'] = result['file'][len(folder)+1:] + result['mtime'] = mtime('') + return translator.order_mpd_track_info(result.items()) def consume_headers(self, result): self.assertEqual(('info_begin',), result[0]) From f486bf512d6ddc81790cf359357597e9e9a92aad Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Nov 2010 23:19:32 +0100 Subject: [PATCH 122/235] Split BaseStoredPlaylistsController in controller and provider --- docs/api/backends.rst | 12 +- mopidy/backends/base/__init__.py | 11 +- mopidy/backends/base/stored_playlists.py | 119 +++++++++++++++--- mopidy/backends/dummy/__init__.py | 10 +- mopidy/backends/libspotify/__init__.py | 8 +- .../backends/libspotify/stored_playlists.py | 4 +- mopidy/backends/local/__init__.py | 10 +- 7 files changed, 139 insertions(+), 35 deletions(-) diff --git a/docs/api/backends.rst b/docs/api/backends.rst index de1957e8..617bcb94 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends.rst @@ -116,8 +116,16 @@ Playback provider :undoc-members: -Backend implementations -======================= +Stored playlists provider +------------------------- + +.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsProvider + :members: + :undoc-members: + + +Backend provider implementations +================================ * :mod:`mopidy.backends.dummy` * :mod:`mopidy.backends.libspotify` diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index eb75467c..7ee86015 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -4,15 +4,16 @@ import random import time from mopidy import settings -from mopidy.backends.base.current_playlist import BaseCurrentPlaylistController -from mopidy.backends.base.library import BaseLibraryController -from mopidy.backends.base.playback import (BasePlaybackController, - BasePlaybackProvider) -from mopidy.backends.base.stored_playlists import BaseStoredPlaylistsController from mopidy.frontends.mpd import translator from mopidy.models import Playlist from mopidy.utils import get_class +from .current_playlist import BaseCurrentPlaylistController +from .library import BaseLibraryController +from .playback import BasePlaybackController, BasePlaybackProvider +from .stored_playlists import (BaseStoredPlaylistsController, + BaseStoredPlaylistsProvider) + logger = logging.getLogger('mopidy.backends.base') __all__ = ['BaseBackend', 'BasePlaybackController', 'BasePlaybackProvider', diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py index 61722c81..cf14db9d 100644 --- a/mopidy/backends/base/stored_playlists.py +++ b/mopidy/backends/base/stored_playlists.py @@ -7,24 +7,30 @@ class BaseStoredPlaylistsController(object): """ :param backend: backend the controller is a part of :type backend: :class:`BaseBackend` + :param provider: provider the controller should use + :type provider: instance of :class:`BaseStoredPlaylistsProvider` """ - def __init__(self, backend): + def __init__(self, backend, provider): self.backend = backend - self._playlists = [] + self.provider = provider def destroy(self): """Cleanup after component.""" - pass + self.provider.destroy() @property def playlists(self): - """List of :class:`mopidy.models.Playlist`.""" - return copy(self._playlists) + """ + Currently stored playlists. + + Read/write. List of :class:`mopidy.models.Playlist`. + """ + return self.provider.playlists @playlists.setter def playlists(self, playlists): - self._playlists = playlists + self.provider.playlists = playlists def create(self, name): """ @@ -34,7 +40,7 @@ class BaseStoredPlaylistsController(object): :type name: string :rtype: :class:`mopidy.models.Playlist` """ - raise NotImplementedError + return self.provider.create(name) def delete(self, playlist): """ @@ -43,7 +49,7 @@ class BaseStoredPlaylistsController(object): :param playlist: the playlist to delete :type playlist: :class:`mopidy.models.Playlist` """ - raise NotImplementedError + return self.provider.delete(playlist) def get(self, **criteria): """ @@ -55,13 +61,14 @@ class BaseStoredPlaylistsController(object): get(name='a') # Returns track with name 'a' get(uri='xyz') # Returns track with URI 'xyz' - get(name='a', uri='xyz') # Returns track with name 'a' and URI 'xyz' + get(name='a', uri='xyz') # Returns track with name 'a' and URI + # 'xyz' :param criteria: one or more criteria to match by :type criteria: dict :rtype: :class:`mopidy.models.Playlist` """ - matches = self._playlists + matches = self.playlists for (key, value) in criteria.iteritems(): matches = filter(lambda p: getattr(p, key) == value, matches) if len(matches) == 1: @@ -82,11 +89,14 @@ class BaseStoredPlaylistsController(object): :type uri: string :rtype: :class:`mopidy.models.Playlist` """ - raise NotImplementedError + return self.provider.lookup(uri) def refresh(self): - """Refresh stored playlists.""" - raise NotImplementedError + """ + Refresh the stored playlists in + :attr:`mopidy.backends.base.BaseStoredPlaylistsController.playlists`. + """ + return self.provider.refresh(uri) def rename(self, playlist, new_name): """ @@ -97,7 +107,7 @@ class BaseStoredPlaylistsController(object): :param new_name: the new name :type new_name: string """ - raise NotImplementedError + return self.provider.rename(playlist, new_name) def save(self, playlist): """ @@ -106,4 +116,85 @@ class BaseStoredPlaylistsController(object): :param playlist: the playlist :type playlist: :class:`mopidy.models.Playlist` """ + return self.provider.save(playlist) + + +class BaseStoredPlaylistsProvider(object): + """ + :param backend: backend the controller is a part of + :type backend: :class:`BaseBackend` + """ + + def __init__(self, backend): + self.backend = backend + self._playlists = [] + + def destroy(self): + """ + Cleanup after component. + + May be overridden by subclasses. + """ + pass + + @property + def playlists(self): + """ + Currently stored playlists. + + Read/write. List of :class:`mopidy.models.Playlist`. + """ + return copy(self._playlists) + + @playlists.setter + def playlists(self, playlists): + self._playlists = playlists + + def create(self, name): + """ + To be overridden by subclass. + + See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.create`. + """ raise NotImplementedError + + def delete(self, playlist): + """ + To be overridden by subclass. + + See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.delete`. + """ + raise NotImplementedError + + def lookup(self, uri): + """ + To be overridden by subclass. + + See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.lookup`. + """ + raise NotImplementedError + + def refresh(self): + """ + To be overridden by subclass. + + See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.refresh`. + """ + raise NotImplementedError + + def rename(self, playlist, new_name): + """ + To be overridden by subclass. + + See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.rename`. + """ + raise NotImplementedError + + def save(self, playlist): + """ + To be overridden by subclass. + + See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.save`. + """ + raise NotImplementedError + diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 7a2788b7..3b94452b 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -1,6 +1,6 @@ from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, BasePlaybackController, BasePlaybackProvider, BaseLibraryController, - BaseStoredPlaylistsController) + BaseStoredPlaylistsController, BaseStoredPlaylistsProvider) from mopidy.models import Playlist class DummyBackend(BaseBackend): @@ -22,7 +22,9 @@ class DummyBackend(BaseBackend): self.playback = DummyPlaybackController(backend=self, provider=playback_provider) - self.stored_playlists = DummyStoredPlaylistsController(backend=self) + stored_playlists_provider = DummyStoredPlaylistsProvider(backend=self) + self.stored_playlists = BaseStoredPlaylistsController(backend=self, + provider=stored_playlists_provider) self.uri_handlers = [u'dummy:'] @@ -75,9 +77,7 @@ class DummyPlaybackProvider(BasePlaybackProvider): return True -class DummyStoredPlaylistsController(BaseStoredPlaylistsController): - _playlists = [] - +class DummyStoredPlaylistsProvider(BaseStoredPlaylistsProvider): def create(self, name): playlist = Playlist(name=name) self._playlists.append(playlist) diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index c067cb67..dbbd4804 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -2,7 +2,7 @@ import logging from mopidy import settings from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, - BasePlaybackController) + BasePlaybackController, BaseStoredPlaylistsController) logger = logging.getLogger('mopidy.backends.libspotify') @@ -36,7 +36,7 @@ class LibspotifyBackend(BaseBackend): def __init__(self, *args, **kwargs): from .library import LibspotifyLibraryController from .playback import LibspotifyPlaybackProvider - from .stored_playlists import LibspotifyStoredPlaylistsController + from .stored_playlists import LibspotifyStoredPlaylistsProvider super(LibspotifyBackend, self).__init__(*args, **kwargs) @@ -48,8 +48,10 @@ class LibspotifyBackend(BaseBackend): self.playback = BasePlaybackController(backend=self, provider=playback_provider) - self.stored_playlists = LibspotifyStoredPlaylistsController( + stored_playlists_provider = LibspotifyStoredPlaylistsProvider( backend=self) + self.stored_playlists = BaseStoredPlaylistsController(backend=self, + provider=stored_playlists_provider) self.uri_handlers = [u'spotify:', u'http://open.spotify.com/'] diff --git a/mopidy/backends/libspotify/stored_playlists.py b/mopidy/backends/libspotify/stored_playlists.py index 3339578c..6f2a7aad 100644 --- a/mopidy/backends/libspotify/stored_playlists.py +++ b/mopidy/backends/libspotify/stored_playlists.py @@ -1,6 +1,6 @@ -from mopidy.backends.base import BaseStoredPlaylistsController +from mopidy.backends.base import BaseStoredPlaylistsProvider -class LibspotifyStoredPlaylistsController(BaseStoredPlaylistsController): +class LibspotifyStoredPlaylistsProvider(BaseStoredPlaylistsProvider): def create(self, name): pass # TODO diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 9796414d..b2c36b72 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -8,7 +8,7 @@ from mopidy import settings from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, BaseLibraryController, BasePlaybackController, BasePlaybackProvider, - BaseStoredPlaylistsController) + BaseStoredPlaylistsController, BaseStoredPlaylistsProvider) from mopidy.models import Playlist, Track, Album from mopidy.utils.process import pickle_connection @@ -34,7 +34,9 @@ class LocalBackend(BaseBackend): self.library = LocalLibraryController(backend=self) - self.stored_playlists = LocalStoredPlaylistsController(backend=self) + stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self) + self.stored_playlists = BaseStoredPlaylistsController(backend=self, + provider=stored_playlists_provider) self.current_playlist = BaseCurrentPlaylistController(backend=self) @@ -74,9 +76,9 @@ class LocalPlaybackProvider(BasePlaybackProvider): return self.backend.output.set_state('READY') -class LocalStoredPlaylistsController(BaseStoredPlaylistsController): +class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider): def __init__(self, *args, **kwargs): - super(LocalStoredPlaylistsController, self).__init__(*args, **kwargs) + super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs) self._folder = os.path.expanduser(settings.LOCAL_PLAYLIST_FOLDER) self.refresh() From dd25fadd8bc45ce2e2670077aac6af0af1b8124e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 1 Nov 2010 23:23:59 +0100 Subject: [PATCH 123/235] Track result should contain key --- mopidy/frontends/mpd/translator.py | 1 + tests/frontends/mpd/serializer_test.py | 1 + 2 files changed, 2 insertions(+) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index e5a55960..e15e1ba5 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -141,6 +141,7 @@ def _add_to_tag_cache(result, folders, files): path = uri_to_path(track_result['file']) track_result['mtime'] = get_mtime(path) track_result['file'] = re.sub(regexp, '', path) + track_result['key'] = os.path.basename(track_result['file']) track_result = order_mpd_track_info(track_result.items()) result.extend(track_result) result.append(('songList end',)) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 6fe50657..77a25e15 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -101,6 +101,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): result = dict(translator.track_to_mpd_format(track)) result['file'] = uri_to_path(result['file']) result['file'] = result['file'][len(folder)+1:] + result['key'] = os.path.basename(result['file']) result['mtime'] = mtime('') return translator.order_mpd_track_info(result.items()) From 7193302c10d04507cf8ac2b577ed20de4a203af4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Nov 2010 23:27:52 +0100 Subject: [PATCH 124/235] Use a DummyQueue in DummyBackend to silence triggered events --- mopidy/backends/dummy/__init__.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 3b94452b..6549b9ec 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -3,6 +3,15 @@ from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, BaseStoredPlaylistsController, BaseStoredPlaylistsProvider) from mopidy.models import Playlist + +class DummyQueue(object): + def __init__(self): + self.received_messages = [] + + def put(self, message): + self.received_messages.append(message) + + class DummyBackend(BaseBackend): """ A backend which implements the backend API in the simplest way possible. @@ -14,12 +23,14 @@ class DummyBackend(BaseBackend): def __init__(self, *args, **kwargs): super(DummyBackend, self).__init__(*args, **kwargs) - self.current_playlist = DummyCurrentPlaylistController(backend=self) + self.core_queue = DummyQueue() + + self.current_playlist = BaseCurrentPlaylistController(backend=self) self.library = DummyLibraryController(backend=self) playback_provider = DummyPlaybackProvider(backend=self) - self.playback = DummyPlaybackController(backend=self, + self.playback = BasePlaybackController(backend=self, provider=playback_provider) stored_playlists_provider = DummyStoredPlaylistsProvider(backend=self) @@ -29,10 +40,6 @@ class DummyBackend(BaseBackend): self.uri_handlers = [u'dummy:'] -class DummyCurrentPlaylistController(BaseCurrentPlaylistController): - pass - - class DummyLibraryController(BaseLibraryController): _library = [] @@ -51,14 +58,6 @@ class DummyLibraryController(BaseLibraryController): return Playlist() -class DummyPlaybackController(BasePlaybackController): - def _trigger_started_playing_event(self): - pass # noop - - def _trigger_stopped_playing_event(self): - pass # noop - - class DummyPlaybackProvider(BasePlaybackProvider): def pause(self): return True From cfe4ec96a6b91258067101c0da5cb2fbb264693a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Nov 2010 23:43:07 +0100 Subject: [PATCH 125/235] docs: Split backend docs into three pages --- docs/api/backends.rst | 132 ------------------------------ docs/api/backends/concepts.rst | 28 +++++++ docs/api/backends/controllers.rst | 65 +++++++++++++++ docs/api/backends/providers.rst | 33 ++++++++ docs/api/index.rst | 5 +- 5 files changed, 130 insertions(+), 133 deletions(-) delete mode 100644 docs/api/backends.rst create mode 100644 docs/api/backends/concepts.rst create mode 100644 docs/api/backends/controllers.rst create mode 100644 docs/api/backends/providers.rst diff --git a/docs/api/backends.rst b/docs/api/backends.rst deleted file mode 100644 index 617bcb94..00000000 --- a/docs/api/backends.rst +++ /dev/null @@ -1,132 +0,0 @@ -********************** -:mod:`mopidy.backends` -********************** - -.. automodule:: mopidy.backends - :synopsis: Backend APIs - - -The backend, controller, and provider concepts -============================================== - -Backend: - The backend is mostly for convenience. It is a container that holds - references to all the controllers. -Controllers: - Each controller has responsibility for a given part of the backend - functionality. Most, but not all, controllers delegates some work to one or - more providers. The controllers are responsible for choosing the right - provider for any given task based upon i.e. the track's URI. -Providers: - Anything specific to i.e. Spotify integration or local storage is contained - in the providers. To integrate with new music sources, you just add new - providers. - -.. digraph:: backend_relations - - Backend -> "Current\nplaylist\ncontroller" - Backend -> "Library\ncontroller" - "Library\ncontroller" -> "Library\nproviders" - Backend -> "Playback\ncontroller" - "Playback\ncontroller" -> "Playback\nproviders" - Backend -> "Stored\nplaylists\ncontroller" - "Stored\nplaylists\ncontroller" -> "Stored\nplaylist\nproviders" - Backend -> Mixer - -.. _backend-controller-api: - -Backend controller API -====================== - -.. note:: - - The backend controller API is the interface that is used by frontends like - :mod:`mopidy.frontends.mpd`. If you want to implement your own backend, see - the :ref:`backend-provider-api`. - -.. autoclass:: mopidy.backends.base.BaseBackend - :members: - :undoc-members: - - -Playback controller -------------------- - -Manages playback, with actions like play, pause, stop, next, previous, and -seek. - -.. autoclass:: mopidy.backends.base.BasePlaybackController - :members: - :undoc-members: - - -Mixer controller ----------------- - -Manages volume. See :class:`mopidy.mixers.BaseMixer`. - - -Current playlist controller ---------------------------- - -Manages everything related to the currently loaded playlist. - -.. autoclass:: mopidy.backends.base.BaseCurrentPlaylistController - :members: - :undoc-members: - - -Stored playlists controller ---------------------------- - -Manages stored playlist. - -.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsController - :members: - :undoc-members: - - -Library controller ------------------- - -Manages the music library, e.g. searching for tracks to be added to a playlist. - -.. autoclass:: mopidy.backends.base.BaseLibraryController - :members: - :undoc-members: - - -.. _backend-provider-api: - -Backend provider API -==================== - -.. note:: - - The backend provider API is the interface that must be implemented when you - create a backend. If you are working on a frontend and need to access the - backend, see the :ref:`backend-controller-api`. - - -Playback provider ------------------ - -.. autoclass:: mopidy.backends.base.BasePlaybackProvider - :members: - :undoc-members: - - -Stored playlists provider -------------------------- - -.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsProvider - :members: - :undoc-members: - - -Backend provider implementations -================================ - -* :mod:`mopidy.backends.dummy` -* :mod:`mopidy.backends.libspotify` -* :mod:`mopidy.backends.local` diff --git a/docs/api/backends/concepts.rst b/docs/api/backends/concepts.rst new file mode 100644 index 00000000..fd7b4d13 --- /dev/null +++ b/docs/api/backends/concepts.rst @@ -0,0 +1,28 @@ +********************************************** +The backend, controller, and provider concepts +********************************************** + +Backend: + The backend is mostly for convenience. It is a container that holds + references to all the controllers. +Controllers: + Each controller has responsibility for a given part of the backend + functionality. Most, but not all, controllers delegates some work to one or + more providers. The controllers are responsible for choosing the right + provider for any given task based upon i.e. the track's URI. See + :ref:`backend-controller-api` for more details. +Providers: + Anything specific to i.e. Spotify integration or local storage is contained + in the providers. To integrate with new music sources, you just add new + providers. See :ref:`backend-provider-api` for more details. + +.. digraph:: backend_relations + + Backend -> "Current\nplaylist\ncontroller" + Backend -> "Library\ncontroller" + "Library\ncontroller" -> "Library\nproviders" + Backend -> "Playback\ncontroller" + "Playback\ncontroller" -> "Playback\nproviders" + Backend -> "Stored\nplaylists\ncontroller" + "Stored\nplaylists\ncontroller" -> "Stored\nplaylist\nproviders" + Backend -> Mixer diff --git a/docs/api/backends/controllers.rst b/docs/api/backends/controllers.rst new file mode 100644 index 00000000..a9d54504 --- /dev/null +++ b/docs/api/backends/controllers.rst @@ -0,0 +1,65 @@ +.. _backend-controller-api: + +********************** +Backend controller API +********************** + + +The backend controller API is the interface that is used by frontends like +:mod:`mopidy.frontends.mpd`. If you want to implement your own backend, see the +:ref:`backend-provider-api`. + + +The backend +=========== + +.. autoclass:: mopidy.backends.base.BaseBackend + :members: + :undoc-members: + + +Playback controller +=================== + +Manages playback, with actions like play, pause, stop, next, previous, and +seek. + +.. autoclass:: mopidy.backends.base.BasePlaybackController + :members: + :undoc-members: + + +Mixer controller +================ + +Manages volume. See :class:`mopidy.mixers.BaseMixer`. + + +Current playlist controller +=========================== + +Manages everything related to the currently loaded playlist. + +.. autoclass:: mopidy.backends.base.BaseCurrentPlaylistController + :members: + :undoc-members: + + +Stored playlists controller +=========================== + +Manages stored playlist. + +.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsController + :members: + :undoc-members: + + +Library controller +================== + +Manages the music library, e.g. searching for tracks to be added to a playlist. + +.. autoclass:: mopidy.backends.base.BaseLibraryController + :members: + :undoc-members: diff --git a/docs/api/backends/providers.rst b/docs/api/backends/providers.rst new file mode 100644 index 00000000..0b042241 --- /dev/null +++ b/docs/api/backends/providers.rst @@ -0,0 +1,33 @@ +.. _backend-provider-api: + +******************** +Backend provider API +******************** + +The backend provider API is the interface that must be implemented when you +create a backend. If you are working on a frontend and need to access the +backend, see the :ref:`backend-controller-api`. + + +Playback provider +================= + +.. autoclass:: mopidy.backends.base.BasePlaybackProvider + :members: + :undoc-members: + + +Stored playlists provider +========================= + +.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsProvider + :members: + :undoc-members: + + +Backend provider implementations +================================ + +* :mod:`mopidy.backends.dummy` +* :mod:`mopidy.backends.libspotify` +* :mod:`mopidy.backends.local` diff --git a/docs/api/index.rst b/docs/api/index.rst index 87ec9bb3..1f37e9ff 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -5,4 +5,7 @@ API reference .. toctree:: :glob: - ** + backends/concepts + backends/controllers + backends/providers + * From 4e0c1ce93fa4ef7881eb33cc8e10cc8d689173e0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Nov 2010 23:52:21 +0100 Subject: [PATCH 126/235] docs: Improve playback provider docs --- mopidy/backends/base/playback.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 8b935c65..1ab8aeda 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -504,8 +504,9 @@ class BasePlaybackProvider(object): def pause(self): """ - To be overridden by subclass. Implement your backend's pause - functionality here. + To be overridden by subclass. + + Pause playback. :rtype: :class:`True` if successful, else :class:`False` """ @@ -513,8 +514,9 @@ class BasePlaybackProvider(object): def play(self, track): """ - To be overridden by subclass. Implement your backend's play - functionality here. + To be overridden by subclass. + + Play given track. :param track: the track to play :type track: :class:`mopidy.models.Track` @@ -524,8 +526,9 @@ class BasePlaybackProvider(object): def resume(self): """ - To be overridden by subclass. Implement your backend's resume - functionality here. + To be overridden by subclass. + + Resume playback at the same time position playback was paused. :rtype: :class:`True` if successful, else :class:`False` """ @@ -533,8 +536,9 @@ class BasePlaybackProvider(object): def seek(self, time_position): """ - To be overridden by subclass. Implement your backend's seek - functionality here. + To be overridden by subclass. + + Seek to a given time position. :param time_position: time position in milliseconds :type time_position: int @@ -544,8 +548,9 @@ class BasePlaybackProvider(object): def stop(self): """ - To be overridden by subclass. Implement your backend's stop - functionality here. + To be overridden by subclass. + + Stop playback. :rtype: :class:`True` if successful, else :class:`False` """ From 8a574a94a583add869281084a67e7dc7835f82cb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Nov 2010 00:26:16 +0100 Subject: [PATCH 127/235] docs: Move available settings to the main settings page --- docs/api/settings.rst | 27 --------------------------- docs/settings.rst | 31 +++++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 29 deletions(-) delete mode 100644 docs/api/settings.rst diff --git a/docs/api/settings.rst b/docs/api/settings.rst deleted file mode 100644 index cfc270d6..00000000 --- a/docs/api/settings.rst +++ /dev/null @@ -1,27 +0,0 @@ -********************** -:mod:`mopidy.settings` -********************** - - -Changing settings -================= - -For any Mopidy installation you will need to change at least a couple of -settings. To do this, create a new file in the ``~/.mopidy/`` directory -named ``settings.py`` and add settings you need to change from their defaults -there. - -A complete ``~/.mopidy/settings.py`` may look like this:: - - MPD_SERVER_HOSTNAME = u'::' - SPOTIFY_USERNAME = u'alice' - SPOTIFY_PASSWORD = u'mysecret' - - -Available settings -================== - -.. automodule:: mopidy.settings - :synopsis: Available settings and their default values - :members: - :undoc-members: diff --git a/docs/settings.rst b/docs/settings.rst index 41507a4a..532f52cf 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -2,13 +2,31 @@ Settings ******** +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. + + +Changing settings +================= + Mopidy reads settings from the file ``~/.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/.mopidy/settings.py``. -You can either create this file yourself, or run the ``mopidy`` command, and it -will create an empty settings file for you. +You can either create the settings 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 +settings you want to change. If you want to keep the default value for setting, +you should *not* redefine it in your own settings file. + +A complete ``~/.mopidy/settings.py`` may look as simple as this:: + + MPD_SERVER_HOSTNAME = u'::' + SPOTIFY_USERNAME = u'alice' + SPOTIFY_PASSWORD = u'mysecret' Music from Spotify @@ -88,3 +106,12 @@ file:: LASTFM_USERNAME = u'myusername' LASTFM_PASSWORD = u'mysecret' + + +Available settings +================== + +.. automodule:: mopidy.settings + :synopsis: Available settings and their default values + :members: + :undoc-members: From 5886dbb0f0788d97afd2f812672af37a24b85b14 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Nov 2010 00:31:38 +0100 Subject: [PATCH 128/235] docs: Cleanup API reference headers --- docs/api/frontends.rst | 14 +++----------- docs/api/mixers.rst | 10 +++------- docs/api/models.rst | 6 +++--- docs/api/outputs.rst | 16 +++++++++------- 4 files changed, 18 insertions(+), 28 deletions(-) diff --git a/docs/api/frontends.rst b/docs/api/frontends.rst index 2f20c72a..0c1e32a3 100644 --- a/docs/api/frontends.rst +++ b/docs/api/frontends.rst @@ -1,6 +1,6 @@ -*********************** -:mod:`mopidy.frontends` -*********************** +************ +Frontend API +************ A frontend may do whatever it wants to, including creating threads, opening TCP ports and exposing Mopidy for a type of clients. @@ -9,14 +9,6 @@ Frontends got one main limitation: they are restricted to passing messages through the ``core_queue`` for all communication with the rest of Mopidy. Thus, the frontend API is very small and reveals little of what a frontend may do. -.. automodule:: mopidy.frontends - :synopsis: Frontend API - :members: - - -Frontend API -============ - .. warning:: A stable frontend API is not available yet, as we've only implemented a diff --git a/docs/api/mixers.rst b/docs/api/mixers.rst index 1d9937ac..434637f3 100644 --- a/docs/api/mixers.rst +++ b/docs/api/mixers.rst @@ -1,6 +1,6 @@ -******************** -:mod:`mopidy.mixers` -******************** +********* +Mixer API +********* Mixers are responsible for controlling volume. Clients of the mixers will simply instantiate a mixer and read/write to the ``volume`` attribute:: @@ -24,10 +24,6 @@ enable one of the hardware device mixers, you must the set :attr:`mopidy.settings.MIXER` setting to point to one of the classes found below, and possibly add some extra settings required by the mixer you choose. - -Mixer API -========= - All mixers should subclass :class:`mopidy.mixers.BaseMixer` and override methods as described below. diff --git a/docs/api/models.rst b/docs/api/models.rst index 62e6f75a..ef11547e 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -1,6 +1,6 @@ -******************** -:mod:`mopidy.models` -******************** +*********** +Data models +*********** These immutable data models are used for all data transfer within the Mopidy backends and between the backends and the MPD frontend. All fields are optional diff --git a/docs/api/outputs.rst b/docs/api/outputs.rst index d8c2932e..5ef1606d 100644 --- a/docs/api/outputs.rst +++ b/docs/api/outputs.rst @@ -1,15 +1,17 @@ -********************* -:mod:`mopidy.outputs` -********************* +********** +Output API +********** Outputs are responsible for playing audio. +.. warning:: -Output API -========== + A stable output API is not available yet, as we've only implemented a + single output module. -A stable output API is not available yet, as we've only implemented a single -output module. +.. automodule:: mopidy.outputs.base + :synopsis: Base class for outputs + :members: Output implementations From d05e48c4391650af8b2a2cd572e4f18ed4489b24 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 2 Nov 2010 00:35:27 +0100 Subject: [PATCH 129/235] Add generic copy method to models (including tests) --- mopidy/models.py | 39 +++++++++++---------------------------- tests/models_test.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 28 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index e691ccb7..7dd75660 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -38,6 +38,17 @@ class ImmutableObject(object): def __ne__(self, other): return not self.__eq__(other) + def copy(self, **kwargs): + data = {} + for key in self.__dict__.keys(): + public_key = key.lstrip('_') + data[public_key] = kwargs.pop(public_key, self.__dict__[key]) + for key in kwargs.keys(): + if hasattr(self, key): + data[key] = kwargs.pop(key) + if kwargs: + raise TypeError("copy() got an unexpected keyword argument '%s'" % key) + return self.__class__(**data) class Artist(ImmutableObject): """ @@ -178,31 +189,3 @@ class Playlist(ImmutableObject): def mpd_format(self, *args, **kwargs): return translator.playlist_to_mpd_format(self, *args, **kwargs) - - def copy(self, uri=None, name=None, tracks=None, last_modified=None): - """ - Create a new playlist object with the given values. The values that are - not given are taken from the object the method is called on. - - Does not change the object on which it is called. - - :param uri: playlist URI - :type uri: string - :param name: playlist name - :type name: string - :param tracks: playlist's tracks - :type tracks: list of :class:`Track` elements - :param last_modified: playlist's modification time - :type last_modified: :class:`datetime.datetime` - :rtype: :class:`Playlist` - """ - if uri is None: - uri = self.uri - if name is None: - name = self.name - if tracks is None: - tracks = self.tracks - if last_modified is None: - last_modified = self.last_modified - return Playlist(uri=uri, name=name, tracks=tracks, - last_modified=last_modified) diff --git a/tests/models_test.py b/tests/models_test.py index 1ccf16ea..2c1dfec7 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -5,6 +5,50 @@ from mopidy.models import Artist, Album, Track, Playlist from tests import SkipTest +class GenericCopyTets(unittest.TestCase): + def compare(self, orig, other): + self.assertEqual(orig, other) + self.assertNotEqual(id(orig), id(other)) + + def test_copying_track(self): + track = Track() + self.compare(track, track.copy()) + + def test_copying_artist(self): + artist = Artist() + self.compare(artist, artist.copy()) + + def test_copying_album(self): + album = Album() + self.compare(album, album.copy()) + + def test_copying_playlist(self): + playlist = Playlist() + self.compare(playlist, playlist.copy()) + + def test_copying_track_with_basic_values(self): + track = Track(name='foo', uri='bar') + copy = track.copy(name='baz') + self.assertEqual('baz', copy.name) + self.assertEqual('bar', copy.uri) + + def test_copying_track_with_missing_values(self): + track = Track(uri='bar') + copy = track.copy(name='baz') + self.assertEqual('baz', copy.name) + self.assertEqual('bar', copy.uri) + + def test_copying_track_with_private_internal_value(self): + artists1 = [Artist(name='foo')] + artists2 = [Artist(name='bar')] + track = Track(artists=artists1) + copy = track.copy(artists=artists2) + self.assertEqual(copy.artists, artists2) + + def test_copying_track_with_invalid_key(self): + test = lambda: Track().copy(invalid_key=True) + self.assertRaises(TypeError, test) + class ArtistTest(unittest.TestCase): def test_uri(self): uri = u'an_uri' From edb2f060c4b56f9f455ed6c63679e90081fbd221 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 1 Nov 2010 23:42:33 +0100 Subject: [PATCH 130/235] Add musicbrainz to models --- mopidy/models.py | 15 ++++++++ tests/models_test.py | 86 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 89 insertions(+), 12 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index 7dd75660..c45d6769 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -56,6 +56,8 @@ class Artist(ImmutableObject): :type uri: string :param name: artist name :type name: string + :param musicbrainz_id: musicbrainz id + :type musicbrainz_id: string """ #: The artist URI. Read-only. @@ -64,6 +66,9 @@ class Artist(ImmutableObject): #: The artist name. Read-only. name = None + #: The musicbrainz id of the artist. Read-only. + musicbrainz_id = None + class Album(ImmutableObject): """ @@ -75,6 +80,8 @@ class Album(ImmutableObject): :type artists: list of :class:`Artist` :param num_tracks: number of tracks in album :type num_tracks: integer + :param musicbrainz_id: musicbrainz id + :type musicbrainz_id: string """ #: The album URI. Read-only. @@ -86,6 +93,9 @@ class Album(ImmutableObject): #: The number of tracks in the album. Read-only. num_tracks = 0 + #: The musicbrainz id of the album. Read-only. + musicbrainz_id = None + def __init__(self, *args, **kwargs): self._artists = frozenset(kwargs.pop('artists', [])) super(Album, self).__init__(*args, **kwargs) @@ -114,6 +124,8 @@ class Track(ImmutableObject): :type length: integer :param bitrate: bitrate in kbit/s :type bitrate: integer + :param musicbrainz_id: musicbrainz id + :type musicbrainz_id: string """ #: The track URI. Read-only. @@ -137,6 +149,9 @@ class Track(ImmutableObject): #: The track's bitrate in kbit/s. Read-only. bitrate = None + #: The musicbrainz id of the track. Read-only. + musicbrainz_id = None + def __init__(self, *args, **kwargs): self._artists = frozenset(kwargs.pop('artists', [])) super(Track, self).__init__(*args, **kwargs) diff --git a/tests/models_test.py b/tests/models_test.py index 2c1dfec7..0b44f337 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -62,6 +62,13 @@ class ArtistTest(unittest.TestCase): self.assertEqual(artist.name, name) self.assertRaises(AttributeError, setattr, artist, 'name', None) + def test_musicbrainz_id(self): + mb_id = u'mb-id' + artist = Artist(musicbrainz_id=mb_id) + self.assertEqual(artist.musicbrainz_id, mb_id) + self.assertRaises(AttributeError, setattr, artist, + 'musicbrainz_id', None) + def test_invalid_kwarg(self): test = lambda: Artist(foo='baz') self.assertRaises(TypeError, test) @@ -78,9 +85,15 @@ class ArtistTest(unittest.TestCase): self.assertEqual(artist1, artist2) self.assertEqual(hash(artist1), hash(artist2)) + def test_eq_musibrainz_id(self): + artist1 = Artist(musicbrainz_id=u'id') + artist2 = Artist(musicbrainz_id=u'id') + self.assertEqual(artist1, artist2) + self.assertEqual(hash(artist1), hash(artist2)) + def test_eq(self): - artist1 = Artist(uri=u'uri', name=u'name') - artist2 = Artist(uri=u'uri', name=u'name') + artist1 = Artist(uri=u'uri', name=u'name', musicbrainz_id='id') + artist2 = Artist(uri=u'uri', name=u'name', musicbrainz_id='id') self.assertEqual(artist1, artist2) self.assertEqual(hash(artist1), hash(artist2)) @@ -102,9 +115,15 @@ class ArtistTest(unittest.TestCase): self.assertNotEqual(artist1, artist2) self.assertNotEqual(hash(artist1), hash(artist2)) + def test_ne_musicbrainz_id(self): + artist1 = Artist(musicbrainz_id=u'id1') + artist2 = Artist(musicbrainz_id=u'id2') + self.assertNotEqual(artist1, artist2) + self.assertNotEqual(hash(artist1), hash(artist2)) + def test_ne(self): - artist1 = Artist(uri=u'uri1', name=u'name1') - artist2 = Artist(uri=u'uri2', name=u'name2') + artist1 = Artist(uri=u'uri1', name=u'name1', musicbrainz_id='id1') + artist2 = Artist(uri=u'uri2', name=u'name2', musicbrainz_id='id2') self.assertNotEqual(artist1, artist2) self.assertNotEqual(hash(artist1), hash(artist2)) @@ -134,6 +153,13 @@ class AlbumTest(unittest.TestCase): self.assertEqual(album.num_tracks, num_tracks) self.assertRaises(AttributeError, setattr, album, 'num_tracks', None) + def test_musicbrainz_id(self): + mb_id = u'mb-id' + album = Album(musicbrainz_id=mb_id) + self.assertEqual(album.musicbrainz_id, mb_id) + self.assertRaises(AttributeError, setattr, album, + 'musicbrainz_id', None) + def test_invalid_kwarg(self): test = lambda: Album(foo='baz') self.assertRaises(TypeError, test) @@ -171,10 +197,16 @@ class AlbumTest(unittest.TestCase): self.assertEqual(album1, album2) self.assertEqual(hash(album1), hash(album2)) + def test_eq_musibrainz_id(self): + album1 = Album(musicbrainz_id=u'id') + album2 = Album(musicbrainz_id=u'id') + self.assertEqual(album1, album2) + self.assertEqual(hash(album1), hash(album2)) + def test_eq(self): artists = [Artist()] - album1 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2) - album2 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2) + album1 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2, musicbrainz_id='id') + album2 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2, musicbrainz_id='id') self.assertEqual(album1, album2) self.assertEqual(hash(album1), hash(album2)) @@ -208,11 +240,19 @@ class AlbumTest(unittest.TestCase): self.assertNotEqual(album1, album2) self.assertNotEqual(hash(album1), hash(album2)) + def test_ne_musicbrainz_id(self): + album1 = Album(musicbrainz_id=u'id1') + album2 = Album(musicbrainz_id=u'id2') + self.assertNotEqual(album1, album2) + self.assertNotEqual(hash(album1), hash(album2)) + def test_ne(self): album1 = Album(name=u'name1', uri=u'uri1', - artists=[Artist(name=u'name1')], num_tracks=1) + artists=[Artist(name=u'name1')], num_tracks=1, + musicbrainz_id='id1') album2 = Album(name=u'name2', uri=u'uri2', - artists=[Artist(name=u'name2')], num_tracks=2) + artists=[Artist(name=u'name2')], num_tracks=2, + musicbrainz_id='id2') self.assertNotEqual(album1, album2) self.assertNotEqual(hash(album1), hash(album2)) @@ -266,6 +306,13 @@ class TrackTest(unittest.TestCase): self.assertEqual(track.bitrate, bitrate) self.assertRaises(AttributeError, setattr, track, 'bitrate', None) + def test_musicbrainz_id(self): + mb_id = u'mb-id' + track = Track(musicbrainz_id=mb_id) + self.assertEqual(track.musicbrainz_id, mb_id) + self.assertRaises(AttributeError, setattr, track, + 'musicbrainz_id', None) + def test_invalid_kwarg(self): test = lambda: Track(foo='baz') self.assertRaises(TypeError, test) @@ -329,14 +376,22 @@ class TrackTest(unittest.TestCase): self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) + def test_eq_musibrainz_id(self): + track1 = Track(musicbrainz_id=u'id') + track2 = Track(musicbrainz_id=u'id') + self.assertEqual(track1, track2) + self.assertEqual(hash(track1), hash(track2)) + def test_eq(self): date = dt.date.today() artists = [Artist()] album = Album() track1 = Track(uri=u'uri', name=u'name', artists=artists, album=album, - track_no=1, date=date, length=100, bitrate=100) + track_no=1, date=date, length=100, bitrate=100, + musicbrainz_id='id') track2 = Track(uri=u'uri', name=u'name', artists=artists, album=album, - track_no=1, date=date, length=100, bitrate=100) + track_no=1, date=date, length=100, bitrate=100, + musicbrainz_id='id') self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) @@ -394,14 +449,21 @@ class TrackTest(unittest.TestCase): self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) + def test_ne_musicbrainz_id(self): + track1 = Track(musicbrainz_id=u'id1') + track2 = Track(musicbrainz_id=u'id2') + self.assertNotEqual(track1, track2) + self.assertNotEqual(hash(track1), hash(track2)) + def test_ne(self): track1 = Track(uri=u'uri1', name=u'name1', artists=[Artist(name=u'name1')], album=Album(name=u'name1'), - track_no=1, date=dt.date.today(), length=100, bitrate=100) + track_no=1, date=dt.date.today(), length=100, bitrate=100, + musicbrainz_id='id1') track2 = Track(uri=u'uri2', name=u'name2', artists=[Artist(name=u'name2')], album=Album(name=u'name2'), track_no=2, date=dt.date.today()-dt.timedelta(days=1), - length=200, bitrate=200) + length=200, bitrate=200, musicbrainz_id='id2') self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) From 3b7904b8265b9ca3ffbd450bdb3e080f7c8d6ee0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 2 Nov 2010 00:12:21 +0100 Subject: [PATCH 131/235] Add FIXME to data translator for scanner --- mopidy/scanner.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 4ccccbdb..7ae40423 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -18,6 +18,8 @@ def translator(data): artist_kwargs = {} track_kwargs = {} + # FIXME replace with data.get('foo', None) ? + if 'album' in data: album_kwargs['name'] = data['album'] From 5974d696f1d91d2323a703795aa7e30d36d8ae77 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 2 Nov 2010 00:12:55 +0100 Subject: [PATCH 132/235] Refactor mpd formater test --- tests/frontends/mpd/serializer_test.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 77a25e15..4313cfda 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -10,6 +10,17 @@ from mopidy.models import Album, Artist, Playlist, Track from tests import data_folder, SkipTest class TrackMpdFormatTest(unittest.TestCase): + track = Track( + uri=u'a uri', + artists=[Artist(name=u'an artist')], + name=u'a name', + album=Album(name=u'an album', num_tracks=13, + artists=[Artist(name=u'an other artist')]), + track_no=7, + date=dt.date(1977, 1, 1), + length=137000, + ) + def setUp(self): settings.LOCAL_MUSIC_PATH = '/dir/subdir' mtime.set_fake_time(1234567) @@ -43,17 +54,7 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Id', 2) in result) def test_track_to_mpd_format_for_nonempty_track(self): - track = Track( - uri=u'a uri', - artists=[Artist(name=u'an artist')], - name=u'a name', - album=Album(name=u'an album', num_tracks=13, - artists=[Artist(name=u'an other artist')]), - track_no=7, - date=dt.date(1977, 1, 1), - length=137000, - ) - result = translator.track_to_mpd_format(track, position=9, cpid=122) + result = translator.track_to_mpd_format(self.track, position=9, cpid=122) self.assert_(('file', 'a uri') in result) self.assert_(('Time', 137) in result) self.assert_(('Artist', 'an artist') in result) From 988ff66c5e7ef480aa143169daee1398bdf21310 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 2 Nov 2010 00:20:59 +0100 Subject: [PATCH 133/235] Add musicbrainz support to scanner translator --- mopidy/scanner.py | 9 +++++++++ tests/scanner_test.py | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 7ae40423..e1899c4b 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -46,6 +46,15 @@ def translator(data): if albumartist_kwargs: album_kwargs['artists'] = [Artist(**albumartist_kwargs)] + if 'musicbrainz-trackid' in data: + track_kwargs['musicbrainz_id'] = data['musicbrainz-trackid'] + + if 'musicbrainz-artistid' in data: + artist_kwargs['musicbrainz_id'] = data['musicbrainz-artistid'] + + if 'musicbrainz-albumid' in data: + album_kwargs['musicbrainz_id'] = data['musicbrainz-albumid'] + track_kwargs['uri'] = data['uri'] track_kwargs['length'] = data['duration'] track_kwargs['album'] = Album(**album_kwargs) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 141f2ceb..a18b64a1 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -25,15 +25,20 @@ class TranslatorTest(unittest.TestCase): 'date': FakeGstDate(2006, 1, 1,), 'container-format': u'ID3 tag', 'duration': 4531, + 'musicbrainz-trackid': 'mbtrackid', + 'musicbrainz-albumid': 'mbalbumid', + 'musicbrainz-artistid': 'mbartistid', } self.album = { 'name': 'albumname', 'num_tracks': 2, + 'musicbrainz_id': 'mbalbumid', } self.artist = { 'name': 'name', + 'musicbrainz_id': 'mbartistid', } self.albumartist = { @@ -46,6 +51,7 @@ class TranslatorTest(unittest.TestCase): 'date': date(2006, 1, 1), 'track_no': 1, 'length': 4531, + 'musicbrainz_id': 'mbtrackid', } def build_track(self): @@ -78,16 +84,31 @@ class TranslatorTest(unittest.TestCase): del self.track['name'] self.check() + def test_missing_track_musicbrainz_id(self): + del self.data['musicbrainz-trackid'] + del self.track['musicbrainz_id'] + self.check() + def test_missing_album_name(self): del self.data['album'] del self.album['name'] self.check() + def test_missing_album_musicbrainz_id(self): + del self.data['musicbrainz-albumid'] + del self.album['musicbrainz_id'] + self.check() + def test_missing_artist_name(self): del self.data['artist'] del self.artist['name'] self.check() + def test_missing_artist_musicbrainz_id(self): + del self.data['musicbrainz-artistid'] + del self.artist['musicbrainz_id'] + self.check() + def test_missing_album_artist(self): del self.data['album-artist'] del self.albumartist['name'] From e7a7d342b8d811d9ec8519e38b740b5b5da1f0e7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 2 Nov 2010 00:21:32 +0100 Subject: [PATCH 134/235] Add tests for musicbrainz in mpd formated tracks --- tests/frontends/mpd/serializer_test.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 4313cfda..164e384f 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -67,6 +67,23 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Id', 122) in result) self.assertEqual(len(result), 10) + def test_track_to_mpd_format_musicbrainz_trackid(self): + track = self.track.copy(musicbrainz_id='foo') + result = translator.track_to_mpd_format(self.track) + self.assert_(('MUSICBRAINZ_TRACKID', 'foo') in result) + + def test_track_to_mpd_format_musicbrainz_albumid(self): + album = self.track.album.copy(musicbrainz_id='foo') + track = self.track.copy(album=album) + result = translator.track_to_mpd_format(self.track) + self.assert_(('MUSICBRAINZ_ALBUMID', 'foo') in result) + + def test_track_to_mpd_format_musicbrainz_artistid(self): + artist = list(self.track.artists)[0].copy(musicbrainz_id='foo') + track = self.track.copy(artists=[artist]) + result = translator.track_to_mpd_format(self.track) + self.assert_(('MUSICBRAINZ_ARTISTID', 'foo') in result) + def test_artists_to_mpd_format(self): artists = [Artist(name=u'ABBA'), Artist(name=u'Beatles')] translated = translator.artists_to_mpd_format(artists) From 31933c2c2fa2c6aac0130070041ee497f50bae49 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 2 Nov 2010 00:43:03 +0100 Subject: [PATCH 135/235] Fix musicbrainz support in mpd format track --- mopidy/frontends/mpd/translator.py | 9 +++++++++ tests/frontends/mpd/serializer_test.py | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index e15e1ba5..48c8fe30 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -41,6 +41,15 @@ def track_to_mpd_format(track, position=None, cpid=None): if position is not None and cpid is not None: result.append(('Pos', position)) result.append(('Id', cpid)) + if track.album is not None and track.album.musicbrainz_id is not None: + result.append(('MUSICBRAINZ_ALBUMID', track.album.musicbrainz_id)) + if track.artists: + artists = filter(lambda a: a.musicbrainz_id is not None, track.artists) + if artists: + # FIXME don't use first and best artist? + result.append(('MUSICBRAINZ_ARTISTID', artists[0].musicbrainz_id)) + if track.musicbrainz_id is not None: + result.append(('MUSICBRAINZ_TRACKID', track.musicbrainz_id)) return result MPD_KEY_ORDER = ''' diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 164e384f..bc01d820 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -69,19 +69,19 @@ class TrackMpdFormatTest(unittest.TestCase): def test_track_to_mpd_format_musicbrainz_trackid(self): track = self.track.copy(musicbrainz_id='foo') - result = translator.track_to_mpd_format(self.track) + result = translator.track_to_mpd_format(track) self.assert_(('MUSICBRAINZ_TRACKID', 'foo') in result) def test_track_to_mpd_format_musicbrainz_albumid(self): album = self.track.album.copy(musicbrainz_id='foo') track = self.track.copy(album=album) - result = translator.track_to_mpd_format(self.track) + result = translator.track_to_mpd_format(track) self.assert_(('MUSICBRAINZ_ALBUMID', 'foo') in result) def test_track_to_mpd_format_musicbrainz_artistid(self): artist = list(self.track.artists)[0].copy(musicbrainz_id='foo') track = self.track.copy(artists=[artist]) - result = translator.track_to_mpd_format(self.track) + result = translator.track_to_mpd_format(track) self.assert_(('MUSICBRAINZ_ARTISTID', 'foo') in result) def test_artists_to_mpd_format(self): From 90fbf69b85ae10fa4495294456e68c4c53d810dc Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 2 Nov 2010 01:00:00 +0100 Subject: [PATCH 136/235] Add albumartistid to scanner --- mopidy/scanner.py | 9 ++++++--- tests/scanner_test.py | 7 +++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index e1899c4b..a7d2e664 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -43,9 +43,6 @@ def translator(data): if 'album-artist' in data: albumartist_kwargs['name'] = data['album-artist'] - if albumartist_kwargs: - album_kwargs['artists'] = [Artist(**albumartist_kwargs)] - if 'musicbrainz-trackid' in data: track_kwargs['musicbrainz_id'] = data['musicbrainz-trackid'] @@ -55,6 +52,12 @@ def translator(data): if 'musicbrainz-albumid' in data: album_kwargs['musicbrainz_id'] = data['musicbrainz-albumid'] + if 'musicbrainz-albumartistid' in data: + albumartist_kwargs['musicbrainz_id'] = data['musicbrainz-albumartistid'] + + if albumartist_kwargs: + album_kwargs['artists'] = [Artist(**albumartist_kwargs)] + track_kwargs['uri'] = data['uri'] track_kwargs['length'] = data['duration'] track_kwargs['album'] = Album(**album_kwargs) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index a18b64a1..a1b53bcf 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -28,6 +28,7 @@ class TranslatorTest(unittest.TestCase): 'musicbrainz-trackid': 'mbtrackid', 'musicbrainz-albumid': 'mbalbumid', 'musicbrainz-artistid': 'mbartistid', + 'musicbrainz-albumartistid': 'mbalbumartistid', } self.album = { @@ -43,6 +44,7 @@ class TranslatorTest(unittest.TestCase): self.albumartist = { 'name': 'albumartistname', + 'musicbrainz_id': 'mbalbumartistid', } self.track = { @@ -114,6 +116,11 @@ class TranslatorTest(unittest.TestCase): del self.albumartist['name'] self.check() + def test_missing_album_artist_musicbrainz_id(self): + del self.data['musicbrainz-albumartistid'] + del self.albumartist['musicbrainz_id'] + self.check() + def test_missing_date(self): del self.data['date'] del self.track['date'] From 37222d66f5b8e11bf70296aebe6885574d1d482a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 2 Nov 2010 01:00:11 +0100 Subject: [PATCH 137/235] Add albumartistid to frontent --- mopidy/frontends/mpd/translator.py | 7 ++++++- tests/frontends/mpd/serializer_test.py | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 48c8fe30..0073c113 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -43,10 +43,15 @@ def track_to_mpd_format(track, position=None, cpid=None): result.append(('Id', cpid)) if track.album is not None and track.album.musicbrainz_id is not None: result.append(('MUSICBRAINZ_ALBUMID', track.album.musicbrainz_id)) + # FIXME don't use first and best artist? + # FIXME don't duplicate following code? + if track.album is not None and track.album.artists: + artists = filter(lambda a: a.musicbrainz_id is not None, track.album.artists) + if artists: + result.append(('MUSICBRAINZ_ALBUMARTISTID', artists[0].musicbrainz_id)) if track.artists: artists = filter(lambda a: a.musicbrainz_id is not None, track.artists) if artists: - # FIXME don't use first and best artist? result.append(('MUSICBRAINZ_ARTISTID', artists[0].musicbrainz_id)) if track.musicbrainz_id is not None: result.append(('MUSICBRAINZ_TRACKID', track.musicbrainz_id)) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index bc01d820..7e4500ea 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -78,6 +78,13 @@ class TrackMpdFormatTest(unittest.TestCase): result = translator.track_to_mpd_format(track) self.assert_(('MUSICBRAINZ_ALBUMID', 'foo') in result) + def test_track_to_mpd_format_musicbrainz_albumid(self): + artist = list(self.track.artists)[0].copy(musicbrainz_id='foo') + album = self.track.album.copy(artists=[artist]) + track = self.track.copy(album=album) + result = translator.track_to_mpd_format(track) + self.assert_(('MUSICBRAINZ_ALBUMARTISTID', 'foo') in result) + def test_track_to_mpd_format_musicbrainz_artistid(self): artist = list(self.track.artists)[0].copy(musicbrainz_id='foo') track = self.track.copy(artists=[artist]) From 8d30b745d260f00026b0ca787a643273140f7b6f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 2 Nov 2010 01:02:41 +0100 Subject: [PATCH 138/235] Add test_musicbrainz_tagcache as reminder that tag cache parser does not know about musicbrainz yet --- tests/backends/local/translator_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py index 2f97e45c..3f2a1c12 100644 --- a/tests/backends/local/translator_test.py +++ b/tests/backends/local/translator_test.py @@ -136,3 +136,6 @@ class MPDTagCacheToTracksTest(unittest.TestCase): data_folder('')) uri = path_to_uri(data_folder('song1.mp3')) self.assertEqual(set([Track(uri=uri, length=4000)]), tracks) + + def test_musicbrainz_tagcache(self): + raise SkipTest From 326ade05cc5fbc3511a95ff9f6dda55fa79ec9a0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 2 Nov 2010 17:24:42 +0100 Subject: [PATCH 139/235] Add musicbrainz support to tag_cache parsing --- mopidy/backends/local/translator.py | 25 +++++++++++++++++++------ tests/backends/local/translator_test.py | 11 +++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 26c2ad6b..0af8f126 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -96,25 +96,31 @@ def _convert_mpd_data(data, tracks, music_dir): track_kwargs = {} album_kwargs = {} + artist_kwargs = {} if 'track' in data: album_kwargs['num_tracks'] = int(data['track'].split('/')[1]) track_kwargs['track_no'] = int(data['track'].split('/')[0]) if 'artist' in data: - artist = Artist(name=data['artist']) - track_kwargs['artists'] = [artist] - album_kwargs['artists'] = [artist] + artist_kwargs['name'] = data['artist'] # FIXME Newer mpd tag caches support albumartist names if 'album' in data: album_kwargs['name'] = data['album'] - album = Album(**album_kwargs) - track_kwargs['album'] = album if 'title' in data: track_kwargs['name'] = data['title'] + if 'musicbrainz_trackid' in data: + track_kwargs['musicbrainz_id'] = data['musicbrainz_trackid'] + + if 'musicbrainz_albumid' in data: + album_kwargs['musicbrainz_id'] = data['musicbrainz_albumid'] + + if 'musicbrainz_artistid' in data: + artist_kwargs['musicbrainz_id'] = data['musicbrainz_artistid'] + # FIXME what if file is uri - generated tag cache needs to allways make # LOCAL_MUSIC_PATH relative paths or this code must handle uris if data['file'][0] == '/': @@ -122,7 +128,14 @@ def _convert_mpd_data(data, tracks, music_dir): else: path = data['file'] - # FIXME newer mpd tag caches provide musicbrainz ids + if artist_kwargs: + artist = Artist(**artist_kwargs) + album_kwargs['artists'] = [artist] + track_kwargs['artists'] = [artist] + + if album_kwargs: + album = Album(**album_kwargs) + track_kwargs['album'] = album track_kwargs['uri'] = path_to_uri(music_dir, path) track_kwargs['length'] = int(data.get('time', 0)) * 1000 diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py index 3f2a1c12..9347db59 100644 --- a/tests/backends/local/translator_test.py +++ b/tests/backends/local/translator_test.py @@ -138,4 +138,15 @@ class MPDTagCacheToTracksTest(unittest.TestCase): self.assertEqual(set([Track(uri=uri, length=4000)]), tracks) def test_musicbrainz_tagcache(self): + tracks = parse_mpd_tag_cache(data_folder('musicbrainz_tag_cache'), + data_folder('')) + artist = list(expected_tracks[0].artists)[0].copy( + musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897') + album = expected_tracks[0].album.copy(artists=[artist], + musicbrainz_id='cb5f1603-d314-4c9c-91e5-e295cfb125d2') + track = expected_tracks[0].copy(artists=[artist], album=album, + musicbrainz_id='90488461-8c1f-4a4e-826b-4c6dc70801f0') + self.assertEqual(track, list(tracks)[0]) + + def test_albumartist_tag_cache(self): raise SkipTest From f58d47fda2ea7ac7d7355fb379de5a8b045e8afa Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 2 Nov 2010 17:41:21 +0100 Subject: [PATCH 140/235] Add fixmes --- mopidy/backends/local/translator.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 0af8f126..8855c1d6 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -105,7 +105,6 @@ def _convert_mpd_data(data, tracks, music_dir): if 'artist' in data: artist_kwargs['name'] = data['artist'] - # FIXME Newer mpd tag caches support albumartist names if 'album' in data: album_kwargs['name'] = data['album'] @@ -121,8 +120,6 @@ def _convert_mpd_data(data, tracks, music_dir): if 'musicbrainz_artistid' in data: artist_kwargs['musicbrainz_id'] = data['musicbrainz_artistid'] - # FIXME what if file is uri - generated tag cache needs to allways make - # LOCAL_MUSIC_PATH relative paths or this code must handle uris if data['file'][0] == '/': path = data['file'][1:] else: From 89b46af983ad99499005a202dc2abefa5c276401 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 2 Nov 2010 17:43:32 +0100 Subject: [PATCH 141/235] Add musicbrainz_tag_cache that was missing --- tests/data/musicbrainz_tag_cache | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/data/musicbrainz_tag_cache diff --git a/tests/data/musicbrainz_tag_cache b/tests/data/musicbrainz_tag_cache new file mode 100644 index 00000000..010242f4 --- /dev/null +++ b/tests/data/musicbrainz_tag_cache @@ -0,0 +1,19 @@ +info_begin +mpd_version: 0.16.0 +fs_charset: UTF-8 +info_end +songList begin +key: song1.mp3 +file: /song1.mp3 +Time: 4 +Artist: name +Title: trackname +Album: albumname +Track: 1/2 +Date: 2006 +MUSICBRAINZ_ALBUMID: cb5f1603-d314-4c9c-91e5-e295cfb125d2 +MUSICBRAINZ_ALBUMARTISTID: 7364dea6-ca9a-48e3-be01-b44ad0d19897 +MUSICBRAINZ_ARTISTID: 7364dea6-ca9a-48e3-be01-b44ad0d19897 +MUSICBRAINZ_TRACKID: 90488461-8c1f-4a4e-826b-4c6dc70801f0 +mtime: 1272319626 +songList end From 6cba4bc5649bdd570c4558c9ea4ba3c9ad9556f7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 2 Nov 2010 20:19:29 +0100 Subject: [PATCH 142/235] Add albumartist support --- mopidy/backends/local/translator.py | 13 ++++++++++++- tests/backends/local/translator_test.py | 9 ++++++++- tests/data/albumartist_tag_cache | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tests/data/albumartist_tag_cache diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 8855c1d6..51522ead 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -97,6 +97,7 @@ def _convert_mpd_data(data, tracks, music_dir): track_kwargs = {} album_kwargs = {} artist_kwargs = {} + albumartist_kwargs = {} if 'track' in data: album_kwargs['num_tracks'] = int(data['track'].split('/')[1]) @@ -104,6 +105,10 @@ def _convert_mpd_data(data, tracks, music_dir): if 'artist' in data: artist_kwargs['name'] = data['artist'] + albumartist_kwargs['name'] = data['artist'] + + if 'albumartist' in data: + albumartist_kwargs['name'] = data['albumartist'] if 'album' in data: album_kwargs['name'] = data['album'] @@ -120,6 +125,9 @@ def _convert_mpd_data(data, tracks, music_dir): if 'musicbrainz_artistid' in data: artist_kwargs['musicbrainz_id'] = data['musicbrainz_artistid'] + if 'musicbrainz_albumartistid' in data: + albumartist_kwargs['musicbrainz_id'] = data['musicbrainz_albumartistid'] + if data['file'][0] == '/': path = data['file'][1:] else: @@ -127,8 +135,11 @@ def _convert_mpd_data(data, tracks, music_dir): if artist_kwargs: artist = Artist(**artist_kwargs) - album_kwargs['artists'] = [artist] track_kwargs['artists'] = [artist] + + if albumartist_kwargs: + albumartist = Artist(**albumartist_kwargs) + album_kwargs['artists'] = [albumartist] if album_kwargs: album = Album(**album_kwargs) diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py index 9347db59..1a3a36b0 100644 --- a/tests/backends/local/translator_test.py +++ b/tests/backends/local/translator_test.py @@ -149,4 +149,11 @@ class MPDTagCacheToTracksTest(unittest.TestCase): self.assertEqual(track, list(tracks)[0]) def test_albumartist_tag_cache(self): - raise SkipTest + tracks = parse_mpd_tag_cache(data_folder('albumartist_tag_cache'), + data_folder('')) + uri = path_to_uri(data_folder('song1.mp3')) + artist = Artist(name='albumartistname') + album = expected_albums[0].copy(artists=[artist]) + track = Track(name='trackname', artists=expected_artists, track_no=1, + album=album, length=4000, uri=uri) + self.assertEqual(track, list(tracks)[0]) diff --git a/tests/data/albumartist_tag_cache b/tests/data/albumartist_tag_cache new file mode 100644 index 00000000..29942a75 --- /dev/null +++ b/tests/data/albumartist_tag_cache @@ -0,0 +1,16 @@ +info_begin +mpd_version: 0.14.2 +fs_charset: UTF-8 +info_end +songList begin +key: song1.mp3 +file: /song1.mp3 +Time: 4 +Artist: name +Title: trackname +Album: albumname +AlbumArtist: albumartistname +Track: 1/2 +Date: 2006 +mtime: 1272319626 +songList end From 9cc053cfd26c752eb65a0a1050992b7bf5ec97e3 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 2 Nov 2010 20:20:01 +0100 Subject: [PATCH 143/235] Add albumartist to musicbrainz test --- tests/backends/local/translator_test.py | 6 +++++- tests/data/musicbrainz_tag_cache | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py index 1a3a36b0..b7fd212c 100644 --- a/tests/backends/local/translator_test.py +++ b/tests/backends/local/translator_test.py @@ -142,10 +142,14 @@ class MPDTagCacheToTracksTest(unittest.TestCase): data_folder('')) artist = list(expected_tracks[0].artists)[0].copy( musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897') - album = expected_tracks[0].album.copy(artists=[artist], + albumartist = list(expected_tracks[0].artists)[0].copy( + name='albumartistname', + musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897') + album = expected_tracks[0].album.copy(artists=[albumartist], musicbrainz_id='cb5f1603-d314-4c9c-91e5-e295cfb125d2') track = expected_tracks[0].copy(artists=[artist], album=album, musicbrainz_id='90488461-8c1f-4a4e-826b-4c6dc70801f0') + self.assertEqual(track, list(tracks)[0]) def test_albumartist_tag_cache(self): diff --git a/tests/data/musicbrainz_tag_cache b/tests/data/musicbrainz_tag_cache index 010242f4..0e9dca46 100644 --- a/tests/data/musicbrainz_tag_cache +++ b/tests/data/musicbrainz_tag_cache @@ -9,6 +9,7 @@ Time: 4 Artist: name Title: trackname Album: albumname +AlbumArtist: albumartistname Track: 1/2 Date: 2006 MUSICBRAINZ_ALBUMID: cb5f1603-d314-4c9c-91e5-e295cfb125d2 From 13ece433634a9697a5921f787fe56a148e7fabef Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Nov 2010 23:07:15 +0100 Subject: [PATCH 144/235] Split BaseLibraryController in controller and provider --- docs/api/backends/providers.rst | 8 +++ mopidy/backends/base/__init__.py | 2 +- mopidy/backends/base/library.py | 61 ++++++++++++++++++-- mopidy/backends/dummy/__init__.py | 13 +++-- mopidy/backends/libspotify/__init__.py | 9 ++- mopidy/backends/libspotify/library.py | 4 +- mopidy/backends/local/__init__.py | 26 +++++---- tests/frontends/mpd/current_playlist_test.py | 8 +-- 8 files changed, 100 insertions(+), 31 deletions(-) diff --git a/docs/api/backends/providers.rst b/docs/api/backends/providers.rst index 0b042241..9289dd06 100644 --- a/docs/api/backends/providers.rst +++ b/docs/api/backends/providers.rst @@ -25,6 +25,14 @@ Stored playlists provider :undoc-members: +Library provider +================ + +.. autoclass:: mopidy.backends.base.BaseLibraryProvider + :members: + :undoc-members: + + Backend provider implementations ================================ diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index 7ee86015..70b17397 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -9,7 +9,7 @@ from mopidy.models import Playlist from mopidy.utils import get_class from .current_playlist import BaseCurrentPlaylistController -from .library import BaseLibraryController +from .library import BaseLibraryController, BaseLibraryProvider from .playback import BasePlaybackController, BasePlaybackProvider from .stored_playlists import (BaseStoredPlaylistsController, BaseStoredPlaylistsProvider) diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py index 94f40863..a1df83cd 100644 --- a/mopidy/backends/base/library.py +++ b/mopidy/backends/base/library.py @@ -8,12 +8,13 @@ class BaseLibraryController(object): :type backend: :class:`BaseBackend` """ - def __init__(self, backend): + def __init__(self, backend, provider): self.backend = backend + self.provider = provider def destroy(self): """Cleanup after component.""" - pass + self.provider.destroy() def find_exact(self, **query): """ @@ -32,7 +33,7 @@ class BaseLibraryController(object): :type query: dict :rtype: :class:`mopidy.models.Playlist` """ - raise NotImplementedError + return self.provider.find_exact(**query) def lookup(self, uri): """ @@ -42,7 +43,7 @@ class BaseLibraryController(object): :type uri: string :rtype: :class:`mopidy.models.Track` or :class:`None` """ - raise NotImplementedError + return self.provider.lookup(uri) def refresh(self, uri=None): """ @@ -51,7 +52,7 @@ class BaseLibraryController(object): :param uri: directory or track URI :type uri: string """ - raise NotImplementedError + return self.provider.refresh(uri) def search(self, **query): """ @@ -70,4 +71,54 @@ class BaseLibraryController(object): :type query: dict :rtype: :class:`mopidy.models.Playlist` """ + return self.provider.search(**query) + + +class BaseLibraryProvider(object): + """ + :param backend: backend the controller is a part of + :type backend: :class:`BaseBackend` + """ + + def __init__(self, backend): + self.backend = backend + + def destroy(self): + """ + Cleanup after component. + + *MAY be implemented by subclasses.* + """ + pass + + def find_exact(self, **query): + """ + See :meth:`mopidy.backends.base.BaseLibraryController.find_exact`. + + *MUST be implemented by subclass.* + """ + raise NotImplementedError + + def lookup(self, uri): + """ + See :meth:`mopidy.backends.base.BaseLibraryController.lookup`. + + *MUST be implemented by subclass.* + """ + raise NotImplementedError + + def refresh(self, uri=None): + """ + See :meth:`mopidy.backends.base.BaseLibraryController.refresh`. + + *MUST be implemented by subclass.* + """ + raise NotImplementedError + + def search(self, **query): + """ + See :meth:`mopidy.backends.base.BaseLibraryController.search`. + + *MUST be implemented by subclass.* + """ raise NotImplementedError diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 6b259338..df4428e3 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -1,6 +1,7 @@ from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, BasePlaybackController, BasePlaybackProvider, BaseLibraryController, - BaseStoredPlaylistsController, BaseStoredPlaylistsProvider) + BaseLibraryProvider, BaseStoredPlaylistsController, + BaseStoredPlaylistsProvider) from mopidy.models import Playlist @@ -27,7 +28,9 @@ class DummyBackend(BaseBackend): self.current_playlist = BaseCurrentPlaylistController(backend=self) - self.library = DummyLibraryController(backend=self) + library_provider = DummyLibraryProvider(backend=self) + self.library = BaseLibraryController(backend=self, + provider=library_provider) playback_provider = DummyPlaybackProvider(backend=self) self.playback = BasePlaybackController(backend=self, @@ -40,8 +43,10 @@ class DummyBackend(BaseBackend): self.uri_handlers = [u'dummy:'] -class DummyLibraryController(BaseLibraryController): - _library = [] +class DummyLibraryProvider(BaseLibraryProvider): + def __init__(self, *args, **kwargs): + super(DummyLibraryProvider, self).__init__(*args, **kwargs) + self._library = [] def find_exact(self, **query): return Playlist() diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index 98b9e884..0eefcd41 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -2,7 +2,8 @@ import logging from mopidy import settings from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, - BasePlaybackController, BaseStoredPlaylistsController) + BaseLibraryController, BasePlaybackController, + BaseStoredPlaylistsController) logger = logging.getLogger('mopidy.backends.libspotify') @@ -34,7 +35,7 @@ class LibspotifyBackend(BaseBackend): # missing spotify dependencies. def __init__(self, *args, **kwargs): - from .library import LibspotifyLibraryController + from .library import LibspotifyLibraryProvider from .playback import LibspotifyPlaybackProvider from .stored_playlists import LibspotifyStoredPlaylistsProvider @@ -42,7 +43,9 @@ class LibspotifyBackend(BaseBackend): self.current_playlist = BaseCurrentPlaylistController(backend=self) - self.library = LibspotifyLibraryController(backend=self) + library_provider = LibspotifyLibraryProvider(backend=self) + self.library = BaseLibraryController(backend=self, + provider=library_provider) playback_provider = LibspotifyPlaybackProvider(backend=self) self.playback = BasePlaybackController(backend=self, diff --git a/mopidy/backends/libspotify/library.py b/mopidy/backends/libspotify/library.py index 972eaf03..948c69b2 100644 --- a/mopidy/backends/libspotify/library.py +++ b/mopidy/backends/libspotify/library.py @@ -3,14 +3,14 @@ import multiprocessing from spotify import Link, SpotifyError -from mopidy.backends.base import BaseLibraryController +from mopidy.backends.base import BaseLibraryProvider from mopidy.backends.libspotify import ENCODING from mopidy.backends.libspotify.translator import LibspotifyTranslator from mopidy.models import Playlist logger = logging.getLogger('mopidy.backends.libspotify.library') -class LibspotifyLibraryController(BaseLibraryController): +class LibspotifyLibraryProvider(BaseLibraryProvider): def find_exact(self, **query): return self.search(**query) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index cf9cbd60..a5b6fb7a 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -5,10 +5,10 @@ import os import shutil from mopidy import settings -from mopidy.backends.base import (BaseBackend, - BaseCurrentPlaylistController, BaseLibraryController, - BasePlaybackController, BasePlaybackProvider, - BaseStoredPlaylistsController, BaseStoredPlaylistsProvider) +from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, + BaseLibraryController, BaseLibraryProvider, BasePlaybackController, + BasePlaybackProvider, BaseStoredPlaylistsController, + BaseStoredPlaylistsProvider) from mopidy.models import Playlist, Track, Album from mopidy.utils.process import pickle_connection @@ -32,18 +32,20 @@ class LocalBackend(BaseBackend): def __init__(self, *args, **kwargs): super(LocalBackend, self).__init__(*args, **kwargs) - self.library = LocalLibraryController(backend=self) - - stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self) - self.stored_playlists = BaseStoredPlaylistsController(backend=self, - provider=stored_playlists_provider) - self.current_playlist = BaseCurrentPlaylistController(backend=self) + library_provider = LocalLibraryProvider(backend=self) + self.library = BaseLibraryController(backend=self, + provider=library_provider) + playback_provider = LocalPlaybackProvider(backend=self) self.playback = LocalPlaybackController(backend=self, provider=playback_provider) + stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self) + self.stored_playlists = BaseStoredPlaylistsController(backend=self, + provider=stored_playlists_provider) + self.uri_handlers = [u'file://'] @@ -149,9 +151,9 @@ class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider): self._playlists.append(playlist) -class LocalLibraryController(BaseLibraryController): +class LocalLibraryProvider(BaseLibraryProvider): def __init__(self, *args, **kwargs): - super(LocalLibraryController, self).__init__(*args, **kwargs) + super(LocalLibraryProvider, self).__init__(*args, **kwargs) self._uri_mapping = {} self.refresh() diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py index 8a4b9ab5..a4179637 100644 --- a/tests/frontends/mpd/current_playlist_test.py +++ b/tests/frontends/mpd/current_playlist_test.py @@ -12,7 +12,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): def test_add(self): needle = Track(uri='dummy://foo') - self.b.library._library = [Track(), Track(), needle, Track()] + self.b.library.provider._library = [Track(), Track(), needle, Track()] self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 5) @@ -40,7 +40,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): def test_addid_without_songpos(self): needle = Track(uri='dummy://foo') - self.b.library._library = [Track(), Track(), needle, Track()] + self.b.library.provider._library = [Track(), Track(), needle, Track()] self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 5) @@ -58,7 +58,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): def test_addid_with_songpos(self): needle = Track(uri='dummy://foo') - self.b.library._library = [Track(), Track(), needle, Track()] + self.b.library.provider._library = [Track(), Track(), needle, Track()] self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 5) @@ -71,7 +71,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): def test_addid_with_songpos_out_of_bounds_should_ack(self): needle = Track(uri='dummy://foo') - self.b.library._library = [Track(), Track(), needle, Track()] + self.b.library.provider._library = [Track(), Track(), needle, Track()] self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 5) From 3e8be009e4ba19b0372cfadb29b960d1e03599e8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Nov 2010 23:33:07 +0100 Subject: [PATCH 145/235] Add consistent may/must notices to all unimplemented methods --- mopidy/backends/base/playback.py | 22 +++++++------- mopidy/backends/base/stored_playlists.py | 26 ++++++++-------- mopidy/frontends/base.py | 14 +++++++-- mopidy/mixers/__init__.py | 4 +-- mopidy/outputs/base.py | 38 +++++++++++++++++++++--- 5 files changed, 72 insertions(+), 32 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 1ab8aeda..1a47391f 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -498,26 +498,26 @@ class BasePlaybackProvider(object): """ Cleanup after component. - May be overridden by subclasses. + *MAY be implemented by subclasses.* """ pass def pause(self): """ - To be overridden by subclass. - Pause playback. + *MUST be implemented by subclass.* + :rtype: :class:`True` if successful, else :class:`False` """ raise NotImplementedError def play(self, track): """ - To be overridden by subclass. - Play given track. + *MUST be implemented by subclass.* + :param track: the track to play :type track: :class:`mopidy.models.Track` :rtype: :class:`True` if successful, else :class:`False` @@ -526,20 +526,20 @@ class BasePlaybackProvider(object): def resume(self): """ - To be overridden by subclass. - Resume playback at the same time position playback was paused. + *MUST be implemented by subclass.* + :rtype: :class:`True` if successful, else :class:`False` """ raise NotImplementedError def seek(self, time_position): """ - To be overridden by subclass. - Seek to a given time position. + *MUST be implemented by subclass.* + :param time_position: time position in milliseconds :type time_position: int :rtype: :class:`True` if successful, else :class:`False` @@ -548,10 +548,10 @@ class BasePlaybackProvider(object): def stop(self): """ - To be overridden by subclass. - Stop playback. + *MUST be implemented by subclass.* + :rtype: :class:`True` if successful, else :class:`False` """ raise NotImplementedError diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py index cf14db9d..dd7380d6 100644 --- a/mopidy/backends/base/stored_playlists.py +++ b/mopidy/backends/base/stored_playlists.py @@ -133,7 +133,7 @@ class BaseStoredPlaylistsProvider(object): """ Cleanup after component. - May be overridden by subclasses. + *MAY be implemented by subclass.* """ pass @@ -152,49 +152,49 @@ class BaseStoredPlaylistsProvider(object): def create(self, name): """ - To be overridden by subclass. - See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.create`. + + *MUST be implemented by subclass.* """ raise NotImplementedError def delete(self, playlist): """ - To be overridden by subclass. - See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.delete`. + + *MUST be implemented by subclass.* """ raise NotImplementedError def lookup(self, uri): """ - To be overridden by subclass. - See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.lookup`. + + *MUST be implemented by subclass.* """ raise NotImplementedError def refresh(self): """ - To be overridden by subclass. - See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.refresh`. + + *MUST be implemented by subclass.* """ raise NotImplementedError def rename(self, playlist, new_name): """ - To be overridden by subclass. - See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.rename`. + + *MUST be implemented by subclass.* """ raise NotImplementedError def save(self, playlist): """ - To be overridden by subclass. - See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.save`. + + *MUST be implemented by subclass.* """ raise NotImplementedError diff --git a/mopidy/frontends/base.py b/mopidy/frontends/base.py index 92545b73..e15c4745 100644 --- a/mopidy/frontends/base.py +++ b/mopidy/frontends/base.py @@ -13,17 +13,27 @@ class BaseFrontend(object): self.backend = backend def start(self): - """Start the frontend.""" + """ + Start the frontend. + + *MAY be implemented by subclass.* + """ pass def destroy(self): - """Destroy the frontend.""" + """ + Destroy the frontend. + + *MAY be implemented by subclass.* + """ pass def process_message(self, message): """ Process messages for the frontend. + *MUST be implemented by subclass.* + :param message: the message :type message: dict """ diff --git a/mopidy/mixers/__init__.py b/mopidy/mixers/__init__.py index 332718a6..231154ee 100644 --- a/mopidy/mixers/__init__.py +++ b/mopidy/mixers/__init__.py @@ -42,7 +42,7 @@ class BaseMixer(object): """ Return volume as integer in range [0, 100]. :class:`None` if unknown. - *Must be implemented by subclass.* + *MUST be implemented by subclass.* """ raise NotImplementedError @@ -50,6 +50,6 @@ class BaseMixer(object): """ Set volume as integer in range [0, 100]. - *Must be implemented by subclass.* + *MUST be implemented by subclass.* """ raise NotImplementedError diff --git a/mopidy/outputs/base.py b/mopidy/outputs/base.py index bb312323..372d7d70 100644 --- a/mopidy/outputs/base.py +++ b/mopidy/outputs/base.py @@ -7,21 +7,35 @@ class BaseOutput(object): self.core_queue = core_queue def start(self): - """Start the output.""" + """ + Start the output. + + *MAY be implemented by subclasses.* + """ pass def destroy(self): - """Destroy the output.""" + """ + Destroy the output. + + *MAY be implemented by subclasses.* + """ pass def process_message(self, message): - """Process messages with the output as destination.""" + """ + Process messages with the output as destination. + + *MUST be implemented by subclass.* + """ raise NotImplementedError def play_uri(self, uri): """ Play URI. + *MUST be implemented by subclass.* + :param uri: the URI to play :type uri: string :rtype: :class:`True` if successful, else :class:`False` @@ -32,19 +46,27 @@ class BaseOutput(object): """ Deliver audio data to be played. + *MUST be implemented by subclass.* + :param capabilities: a GStreamer capabilities string :type capabilities: string """ raise NotImplementedError def end_of_data_stream(self): - """Signal that the last audio data has been delivered.""" + """ + Signal that the last audio data has been delivered. + + *MUST be implemented by subclass.* + """ raise NotImplementedError def get_position(self): """ Get position in milliseconds. + *MUST be implemented by subclass.* + :rtype: int """ raise NotImplementedError @@ -53,6 +75,8 @@ class BaseOutput(object): """ Set position in milliseconds. + *MUST be implemented by subclass.* + :param position: the position in milliseconds :type volume: int :rtype: :class:`True` if successful, else :class:`False` @@ -63,6 +87,8 @@ class BaseOutput(object): """ Set playback state. + *MUST be implemented by subclass.* + :param state: the state :type state: string :rtype: :class:`True` if successful, else :class:`False` @@ -73,6 +99,8 @@ class BaseOutput(object): """ Get volume level for software mixer. + *MUST be implemented by subclass.* + :rtype: int in range [0..100] """ raise NotImplementedError @@ -81,6 +109,8 @@ class BaseOutput(object): """ Set volume level for software mixer. + *MUST be implemented by subclass.* + :param volume: the volume in the range [0..100] :type volume: int :rtype: :class:`True` if successful, else :class:`False` From 9267cd43cdb34183dacff3863981b6e207affaf0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Nov 2010 23:35:56 +0100 Subject: [PATCH 146/235] Move BaseMixer from __init__.py to base.py --- mopidy/mixers/__init__.py | 55 ----------------------------- mopidy/mixers/alsa.py | 2 +- mopidy/mixers/base.py | 55 +++++++++++++++++++++++++++++ mopidy/mixers/denon.py | 2 +- mopidy/mixers/dummy.py | 2 +- mopidy/mixers/gstreamer_software.py | 2 +- mopidy/mixers/nad.py | 2 +- mopidy/mixers/osa.py | 2 +- 8 files changed, 61 insertions(+), 61 deletions(-) create mode 100644 mopidy/mixers/base.py diff --git a/mopidy/mixers/__init__.py b/mopidy/mixers/__init__.py index 231154ee..e69de29b 100644 --- a/mopidy/mixers/__init__.py +++ b/mopidy/mixers/__init__.py @@ -1,55 +0,0 @@ -from mopidy import settings - -class BaseMixer(object): - """ - :param backend: a backend instance - :type mixer: :class:`mopidy.backends.base.BaseBackend` - - **Settings:** - - - :attr:`mopidy.settings.MIXER_MAX_VOLUME` - """ - - def __init__(self, backend, *args, **kwargs): - self.backend = backend - self.amplification_factor = settings.MIXER_MAX_VOLUME / 100.0 - - @property - def volume(self): - """ - The audio volume - - Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is - equal to 0. Values above 100 is equal to 100. - """ - if self._get_volume() is None: - return None - return int(self._get_volume() / self.amplification_factor) - - @volume.setter - def volume(self, volume): - volume = int(int(volume) * self.amplification_factor) - if volume < 0: - volume = 0 - elif volume > 100: - volume = 100 - self._set_volume(volume) - - def destroy(self): - pass - - def _get_volume(self): - """ - Return volume as integer in range [0, 100]. :class:`None` if unknown. - - *MUST be implemented by subclass.* - """ - raise NotImplementedError - - def _set_volume(self, volume): - """ - Set volume as integer in range [0, 100]. - - *MUST be implemented by subclass.* - """ - raise NotImplementedError diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py index 6eef6da4..f90060ce 100644 --- a/mopidy/mixers/alsa.py +++ b/mopidy/mixers/alsa.py @@ -2,7 +2,7 @@ import alsaaudio import logging from mopidy import settings -from mopidy.mixers import BaseMixer +from mopidy.mixers.base import BaseMixer logger = logging.getLogger('mopidy.mixers.alsa') diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py new file mode 100644 index 00000000..231154ee --- /dev/null +++ b/mopidy/mixers/base.py @@ -0,0 +1,55 @@ +from mopidy import settings + +class BaseMixer(object): + """ + :param backend: a backend instance + :type mixer: :class:`mopidy.backends.base.BaseBackend` + + **Settings:** + + - :attr:`mopidy.settings.MIXER_MAX_VOLUME` + """ + + def __init__(self, backend, *args, **kwargs): + self.backend = backend + self.amplification_factor = settings.MIXER_MAX_VOLUME / 100.0 + + @property + def volume(self): + """ + The audio volume + + Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is + equal to 0. Values above 100 is equal to 100. + """ + if self._get_volume() is None: + return None + return int(self._get_volume() / self.amplification_factor) + + @volume.setter + def volume(self, volume): + volume = int(int(volume) * self.amplification_factor) + if volume < 0: + volume = 0 + elif volume > 100: + volume = 100 + self._set_volume(volume) + + def destroy(self): + pass + + def _get_volume(self): + """ + Return volume as integer in range [0, 100]. :class:`None` if unknown. + + *MUST be implemented by subclass.* + """ + raise NotImplementedError + + def _set_volume(self, volume): + """ + Set volume as integer in range [0, 100]. + + *MUST be implemented by subclass.* + """ + raise NotImplementedError diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index 32750f60..e6d752b6 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -4,7 +4,7 @@ from threading import Lock from serial import Serial from mopidy import settings -from mopidy.mixers import BaseMixer +from mopidy.mixers.base import BaseMixer logger = logging.getLogger(u'mopidy.mixers.denon') diff --git a/mopidy/mixers/dummy.py b/mopidy/mixers/dummy.py index b0ea0e47..12a8137e 100644 --- a/mopidy/mixers/dummy.py +++ b/mopidy/mixers/dummy.py @@ -1,4 +1,4 @@ -from mopidy.mixers import BaseMixer +from mopidy.mixers.base import BaseMixer class DummyMixer(BaseMixer): """Mixer which just stores and reports the chosen volume.""" diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py index 333690ea..9dca3690 100644 --- a/mopidy/mixers/gstreamer_software.py +++ b/mopidy/mixers/gstreamer_software.py @@ -1,4 +1,4 @@ -from mopidy.mixers import BaseMixer +from mopidy.mixers.base import BaseMixer class GStreamerSoftwareMixer(BaseMixer): """Mixer which uses GStreamer to control volume in software.""" diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index 8caa9700..3215a761 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -3,7 +3,7 @@ from serial import Serial from multiprocessing import Pipe from mopidy import settings -from mopidy.mixers import BaseMixer +from mopidy.mixers.base import BaseMixer from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.mixers.nad') diff --git a/mopidy/mixers/osa.py b/mopidy/mixers/osa.py index 3aeaed5c..8d69eb47 100644 --- a/mopidy/mixers/osa.py +++ b/mopidy/mixers/osa.py @@ -1,7 +1,7 @@ from subprocess import Popen, PIPE import time -from mopidy.mixers import BaseMixer +from mopidy.mixers.base import BaseMixer class OsaMixer(BaseMixer): """Mixer which uses ``osascript`` on OS X to control volume.""" From 6a8998a5c38241154b25bb0038b01a2ba3e67652 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Nov 2010 23:42:02 +0100 Subject: [PATCH 147/235] Fix pylint warnings --- mopidy/core.py | 1 + mopidy/frontends/mpd/translator.py | 2 +- mopidy/scanner.py | 4 +--- mopidy/utils/path.py | 3 +++ 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index 0be6b96f..1a4ed7cc 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -18,6 +18,7 @@ class CoreProcess(BaseThread): super(CoreProcess, self).__init__(self.core_queue) self.name = 'CoreProcess' self.options = self.parse_options() + self.gobject_loop = None self.output = None self.backend = None self.frontends = [] diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index e15e1ba5..a2fa381f 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -4,7 +4,7 @@ import re from mopidy import settings from mopidy.utils.path import mtime as get_mtime from mopidy.frontends.mpd import protocol -from mopidy.utils.path import path_to_uri, uri_to_path, split_path +from mopidy.utils.path import uri_to_path, split_path def track_to_mpd_format(track, position=None, cpid=None): """ diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 4ccccbdb..8c88d5d0 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -6,8 +6,6 @@ pygst.require('0.10') import gst import datetime -import sys -import threading from mopidy.utils.path import path_to_uri, find_files from mopidy.models import Track, Artist, Album @@ -25,7 +23,7 @@ def translator(data): album_kwargs['num_tracks'] = data['track-count'] if 'artist' in data: - artist_kwargs['name'] =data['artist'] + artist_kwargs['name'] = data['artist'] if 'date' in data: date = data['date'] diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index f25d754a..540cb4fa 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -44,6 +44,8 @@ def split_path(path): break return parts +# pylint: disable = W0612 +# Unused variable 'dirnames' def find_files(path): if os.path.isfile(path): if not isinstance(path, unicode): @@ -56,6 +58,7 @@ def find_files(path): if not isinstance(filename, unicode): filename = filename.decode('utf-8') yield filename +# pylint: enable = W0612 class Mtime(object): def __init__(self): From 0836418bfcfa30516489f5b783aa426e58a50476 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Nov 2010 23:42:58 +0100 Subject: [PATCH 148/235] Fix pylint error --- mopidy/backends/base/stored_playlists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py index dd7380d6..bb86c92e 100644 --- a/mopidy/backends/base/stored_playlists.py +++ b/mopidy/backends/base/stored_playlists.py @@ -96,7 +96,7 @@ class BaseStoredPlaylistsController(object): Refresh the stored playlists in :attr:`mopidy.backends.base.BaseStoredPlaylistsController.playlists`. """ - return self.provider.refresh(uri) + return self.provider.refresh() def rename(self, playlist, new_name): """ From c82b0d01aad3571d748cf092dae304b22c1a90de Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Nov 2010 23:46:47 +0100 Subject: [PATCH 149/235] We do not need __all__, cause we do not 'import *' --- mopidy/backends/base/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index 70b17397..f4a54984 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -16,10 +16,6 @@ from .stored_playlists import (BaseStoredPlaylistsController, logger = logging.getLogger('mopidy.backends.base') -__all__ = ['BaseBackend', 'BasePlaybackController', 'BasePlaybackProvider', - 'BaseCurrentPlaylistController', 'BaseStoredPlaylistsController', - 'BaseLibraryController'] - class BaseBackend(object): """ :param core_queue: a queue for sending messages to From 79b61895cc89839fca35ac6ad720253fc790c3ff Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Nov 2010 23:49:15 +0100 Subject: [PATCH 150/235] Remove unmaintained tests --- tests/backends/libspotify/__init__.py | 0 .../libspotify/backend_integrationtest.py | 44 ------------------- 2 files changed, 44 deletions(-) delete mode 100644 tests/backends/libspotify/__init__.py delete mode 100644 tests/backends/libspotify/backend_integrationtest.py diff --git a/tests/backends/libspotify/__init__.py b/tests/backends/libspotify/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/backends/libspotify/backend_integrationtest.py b/tests/backends/libspotify/backend_integrationtest.py deleted file mode 100644 index 8d1f0b0e..00000000 --- a/tests/backends/libspotify/backend_integrationtest.py +++ /dev/null @@ -1,44 +0,0 @@ -# TODO This integration test is work in progress. - -import unittest - -from mopidy.backends.libspotify import LibspotifyBackend -from mopidy.models import Track - -from tests.backends.base.current_playlist import \ - BaseCurrentPlaylistControllerTest -from tests.backends.base.library import BaseLibraryControllerTest -from tests.backends.base.playback import BasePlaybackControllerTest -from tests.backends.base.stored_playlists import \ - BaseStoredPlaylistsControllerTest - -uris = [ - 'spotify:track:6vqcpVcbI3Zu6sH3ieLDNt', - 'spotify:track:111sulhaZqgsnypz3MkiaW', - 'spotify:track:7t8oznvbeiAPMDRuK0R5ZT', -] - -class LibspotifyCurrentPlaylistControllerTest( - BaseCurrentPlaylistControllerTest, unittest.TestCase): - - backend_class = LibspotifyBackend - tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)] - - -class LibspotifyPlaybackControllerTest( - BasePlaybackControllerTest, unittest.TestCase): - - backend_class = LibspotifyBackend - tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)] - - -class LibspotifyStoredPlaylistsControllerTest( - BaseStoredPlaylistsControllerTest, unittest.TestCase): - - backend_class = LibspotifyBackend - - -class LibspotifyLibraryControllerTest( - BaseLibraryControllerTest, unittest.TestCase): - - backend_class = LibspotifyBackend From 67885e83b6076624c7a8870c08f8165bb8677a97 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Nov 2010 23:50:47 +0100 Subject: [PATCH 151/235] Rename {Base => }CurrentPlaylistController --- docs/api/backends/controllers.rst | 2 +- mopidy/backends/base/__init__.py | 4 ++-- mopidy/backends/base/current_playlist.py | 2 +- mopidy/backends/base/playback.py | 2 +- mopidy/backends/dummy/__init__.py | 4 ++-- mopidy/backends/libspotify/__init__.py | 4 ++-- mopidy/backends/local/__init__.py | 4 ++-- tests/backends/base/current_playlist.py | 2 +- tests/backends/local/current_playlist_test.py | 5 ++--- 9 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/api/backends/controllers.rst b/docs/api/backends/controllers.rst index a9d54504..16ecc9b6 100644 --- a/docs/api/backends/controllers.rst +++ b/docs/api/backends/controllers.rst @@ -40,7 +40,7 @@ Current playlist controller Manages everything related to the currently loaded playlist. -.. autoclass:: mopidy.backends.base.BaseCurrentPlaylistController +.. autoclass:: mopidy.backends.base.CurrentPlaylistController :members: :undoc-members: diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index f4a54984..39ad6e9e 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -8,7 +8,7 @@ from mopidy.frontends.mpd import translator from mopidy.models import Playlist from mopidy.utils import get_class -from .current_playlist import BaseCurrentPlaylistController +from .current_playlist import CurrentPlaylistController from .library import BaseLibraryController, BaseLibraryProvider from .playback import BasePlaybackController, BasePlaybackProvider from .stored_playlists import (BaseStoredPlaylistsController, @@ -42,7 +42,7 @@ class BaseBackend(object): core_queue = None #: The current playlist controller. An instance of - #: :class:`mopidy.backends.base.BaseCurrentPlaylistController`. + #: :class:`mopidy.backends.base.CurrentPlaylistController`. current_playlist = None #: The library controller. An instance of diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index 34a16369..fec8e5a3 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -6,7 +6,7 @@ from mopidy.frontends.mpd import translator logger = logging.getLogger('mopidy.backends.base') -class BaseCurrentPlaylistController(object): +class CurrentPlaylistController(object): """ :param backend: backend the controller is a part of :type backend: :class:`BaseBackend` diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 1a47391f..fa848845 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -331,7 +331,7 @@ class BasePlaybackController(object): """ Tell the playback controller that the current playlist has changed. - Used by :class:`mopidy.backends.base.BaseCurrentPlaylistController`. + Used by :class:`mopidy.backends.base.CurrentPlaylistController`. """ self._first_shuffle = True self._shuffled = [] diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index df4428e3..00f8c9f2 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -1,4 +1,4 @@ -from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, +from mopidy.backends.base import (BaseBackend, CurrentPlaylistController, BasePlaybackController, BasePlaybackProvider, BaseLibraryController, BaseLibraryProvider, BaseStoredPlaylistsController, BaseStoredPlaylistsProvider) @@ -26,7 +26,7 @@ class DummyBackend(BaseBackend): self.core_queue = DummyQueue() - self.current_playlist = BaseCurrentPlaylistController(backend=self) + self.current_playlist = CurrentPlaylistController(backend=self) library_provider = DummyLibraryProvider(backend=self) self.library = BaseLibraryController(backend=self, diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index 0eefcd41..450ac7be 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -1,7 +1,7 @@ import logging from mopidy import settings -from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, +from mopidy.backends.base import (BaseBackend, CurrentPlaylistController, BaseLibraryController, BasePlaybackController, BaseStoredPlaylistsController) @@ -41,7 +41,7 @@ class LibspotifyBackend(BaseBackend): super(LibspotifyBackend, self).__init__(*args, **kwargs) - self.current_playlist = BaseCurrentPlaylistController(backend=self) + self.current_playlist = CurrentPlaylistController(backend=self) library_provider = LibspotifyLibraryProvider(backend=self) self.library = BaseLibraryController(backend=self, diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index a5b6fb7a..0c624687 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -5,7 +5,7 @@ import os import shutil from mopidy import settings -from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, +from mopidy.backends.base import (BaseBackend, CurrentPlaylistController, BaseLibraryController, BaseLibraryProvider, BasePlaybackController, BasePlaybackProvider, BaseStoredPlaylistsController, BaseStoredPlaylistsProvider) @@ -32,7 +32,7 @@ class LocalBackend(BaseBackend): def __init__(self, *args, **kwargs): super(LocalBackend, self).__init__(*args, **kwargs) - self.current_playlist = BaseCurrentPlaylistController(backend=self) + self.current_playlist = CurrentPlaylistController(backend=self) library_provider = LocalLibraryProvider(backend=self) self.library = BaseLibraryController(backend=self, diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py index 05f08e18..2b6cb84e 100644 --- a/tests/backends/base/current_playlist.py +++ b/tests/backends/base/current_playlist.py @@ -9,7 +9,7 @@ from mopidy.utils import get_class from tests.backends.base import populate_playlist -class BaseCurrentPlaylistControllerTest(object): +class CurrentPlaylistControllerTest(object): tracks = [] def setUp(self): diff --git a/tests/backends/local/current_playlist_test.py b/tests/backends/local/current_playlist_test.py index 3895497a..6f72d7d5 100644 --- a/tests/backends/local/current_playlist_test.py +++ b/tests/backends/local/current_playlist_test.py @@ -10,11 +10,10 @@ from mopidy import settings from mopidy.backends.local import LocalBackend from mopidy.models import Track -from tests.backends.base.current_playlist import \ - BaseCurrentPlaylistControllerTest +from tests.backends.base.current_playlist import CurrentPlaylistControllerTest from tests.backends.local import generate_song -class LocalCurrentPlaylistControllerTest(BaseCurrentPlaylistControllerTest, +class LocalCurrentPlaylistControllerTest(CurrentPlaylistControllerTest, unittest.TestCase): backend_class = LocalBackend From 5ada4709bdfd5a20ec809edd4f64817ce662a608 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Nov 2010 23:54:02 +0100 Subject: [PATCH 152/235] Rename {Base => }LibraryController --- docs/api/backends/controllers.rst | 2 +- mopidy/backends/base/__init__.py | 4 ++-- mopidy/backends/base/library.py | 10 +++++----- mopidy/backends/dummy/__init__.py | 4 ++-- mopidy/backends/libspotify/__init__.py | 5 ++--- mopidy/backends/local/__init__.py | 4 ++-- tests/backends/base/library.py | 2 +- tests/backends/local/library_test.py | 4 ++-- 8 files changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/api/backends/controllers.rst b/docs/api/backends/controllers.rst index 16ecc9b6..d3346a27 100644 --- a/docs/api/backends/controllers.rst +++ b/docs/api/backends/controllers.rst @@ -60,6 +60,6 @@ Library controller Manages the music library, e.g. searching for tracks to be added to a playlist. -.. autoclass:: mopidy.backends.base.BaseLibraryController +.. autoclass:: mopidy.backends.base.LibraryController :members: :undoc-members: diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index 39ad6e9e..529c2179 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -9,7 +9,7 @@ from mopidy.models import Playlist from mopidy.utils import get_class from .current_playlist import CurrentPlaylistController -from .library import BaseLibraryController, BaseLibraryProvider +from .library import LibraryController, BaseLibraryProvider from .playback import BasePlaybackController, BasePlaybackProvider from .stored_playlists import (BaseStoredPlaylistsController, BaseStoredPlaylistsProvider) @@ -46,7 +46,7 @@ class BaseBackend(object): current_playlist = None #: The library controller. An instance of - # :class:`mopidy.backends.base.BaseLibraryController`. + # :class:`mopidy.backends.base.LibraryController`. library = None #: The sound mixer. An instance of :class:`mopidy.mixers.BaseMixer`. diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py index a1df83cd..697d3554 100644 --- a/mopidy/backends/base/library.py +++ b/mopidy/backends/base/library.py @@ -2,7 +2,7 @@ import logging logger = logging.getLogger('mopidy.backends.base') -class BaseLibraryController(object): +class LibraryController(object): """ :param backend: backend the controller is a part of :type backend: :class:`BaseBackend` @@ -93,7 +93,7 @@ class BaseLibraryProvider(object): def find_exact(self, **query): """ - See :meth:`mopidy.backends.base.BaseLibraryController.find_exact`. + See :meth:`mopidy.backends.base.LibraryController.find_exact`. *MUST be implemented by subclass.* """ @@ -101,7 +101,7 @@ class BaseLibraryProvider(object): def lookup(self, uri): """ - See :meth:`mopidy.backends.base.BaseLibraryController.lookup`. + See :meth:`mopidy.backends.base.LibraryController.lookup`. *MUST be implemented by subclass.* """ @@ -109,7 +109,7 @@ class BaseLibraryProvider(object): def refresh(self, uri=None): """ - See :meth:`mopidy.backends.base.BaseLibraryController.refresh`. + See :meth:`mopidy.backends.base.LibraryController.refresh`. *MUST be implemented by subclass.* """ @@ -117,7 +117,7 @@ class BaseLibraryProvider(object): def search(self, **query): """ - See :meth:`mopidy.backends.base.BaseLibraryController.search`. + See :meth:`mopidy.backends.base.LibraryController.search`. *MUST be implemented by subclass.* """ diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 00f8c9f2..62a04567 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -1,5 +1,5 @@ from mopidy.backends.base import (BaseBackend, CurrentPlaylistController, - BasePlaybackController, BasePlaybackProvider, BaseLibraryController, + BasePlaybackController, BasePlaybackProvider, LibraryController, BaseLibraryProvider, BaseStoredPlaylistsController, BaseStoredPlaylistsProvider) from mopidy.models import Playlist @@ -29,7 +29,7 @@ class DummyBackend(BaseBackend): self.current_playlist = CurrentPlaylistController(backend=self) library_provider = DummyLibraryProvider(backend=self) - self.library = BaseLibraryController(backend=self, + self.library = LibraryController(backend=self, provider=library_provider) playback_provider = DummyPlaybackProvider(backend=self) diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index 450ac7be..59ff1fde 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -2,8 +2,7 @@ import logging from mopidy import settings from mopidy.backends.base import (BaseBackend, CurrentPlaylistController, - BaseLibraryController, BasePlaybackController, - BaseStoredPlaylistsController) + LibraryController, BasePlaybackController, BaseStoredPlaylistsController) logger = logging.getLogger('mopidy.backends.libspotify') @@ -44,7 +43,7 @@ class LibspotifyBackend(BaseBackend): self.current_playlist = CurrentPlaylistController(backend=self) library_provider = LibspotifyLibraryProvider(backend=self) - self.library = BaseLibraryController(backend=self, + self.library = LibraryController(backend=self, provider=library_provider) playback_provider = LibspotifyPlaybackProvider(backend=self) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 0c624687..ef42f4c4 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -6,7 +6,7 @@ import shutil from mopidy import settings from mopidy.backends.base import (BaseBackend, CurrentPlaylistController, - BaseLibraryController, BaseLibraryProvider, BasePlaybackController, + LibraryController, BaseLibraryProvider, BasePlaybackController, BasePlaybackProvider, BaseStoredPlaylistsController, BaseStoredPlaylistsProvider) from mopidy.models import Playlist, Track, Album @@ -35,7 +35,7 @@ class LocalBackend(BaseBackend): self.current_playlist = CurrentPlaylistController(backend=self) library_provider = LocalLibraryProvider(backend=self) - self.library = BaseLibraryController(backend=self, + self.library = LibraryController(backend=self, provider=library_provider) playback_provider = LocalPlaybackProvider(backend=self) diff --git a/tests/backends/base/library.py b/tests/backends/base/library.py index 1239bd08..71f62147 100644 --- a/tests/backends/base/library.py +++ b/tests/backends/base/library.py @@ -3,7 +3,7 @@ from mopidy.models import Playlist, Track, Album, Artist from tests import SkipTest, data_folder -class BaseLibraryControllerTest(object): +class LibraryControllerTest(object): artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()] albums = [Album(name='album1', artists=artists[:1]), Album(name='album2', artists=artists[1:2]), diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index 34465d09..0c44924a 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -10,9 +10,9 @@ from mopidy import settings from mopidy.backends.local import LocalBackend from tests import data_folder -from tests.backends.base.library import BaseLibraryControllerTest +from tests.backends.base.library import LibraryControllerTest -class LocalLibraryControllerTest(BaseLibraryControllerTest, unittest.TestCase): +class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase): backend_class = LocalBackend From fa4818095f3e97039203f3a6622463ef70c19a7a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Nov 2010 23:56:15 +0100 Subject: [PATCH 153/235] Document 'provider' arg to LibraryController() --- mopidy/backends/base/library.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py index 697d3554..f3477489 100644 --- a/mopidy/backends/base/library.py +++ b/mopidy/backends/base/library.py @@ -6,6 +6,8 @@ class LibraryController(object): """ :param backend: backend the controller is a part of :type backend: :class:`BaseBackend` + :param provider: provider the controller should use + :type provider: instance of :class:`BaseLibraryProvider` """ def __init__(self, backend, provider): From 76ce2a63450e5c4efbdab612ac10e9444f99b091 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Nov 2010 23:58:10 +0100 Subject: [PATCH 154/235] Rename {Base => }StoredPlaylistController --- docs/api/backends/controllers.rst | 2 +- mopidy/backends/base/__init__.py | 4 ++-- mopidy/backends/base/stored_playlists.py | 16 ++++++++-------- mopidy/backends/dummy/__init__.py | 4 ++-- mopidy/backends/libspotify/__init__.py | 4 ++-- mopidy/backends/local/__init__.py | 4 ++-- tests/backends/base/stored_playlists.py | 2 +- tests/backends/local/stored_playlists_test.py | 4 ++-- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/api/backends/controllers.rst b/docs/api/backends/controllers.rst index d3346a27..97ce2508 100644 --- a/docs/api/backends/controllers.rst +++ b/docs/api/backends/controllers.rst @@ -50,7 +50,7 @@ Stored playlists controller Manages stored playlist. -.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsController +.. autoclass:: mopidy.backends.base.StoredPlaylistsController :members: :undoc-members: diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index 529c2179..f1fdfcbe 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -11,7 +11,7 @@ from mopidy.utils import get_class from .current_playlist import CurrentPlaylistController from .library import LibraryController, BaseLibraryProvider from .playback import BasePlaybackController, BasePlaybackProvider -from .stored_playlists import (BaseStoredPlaylistsController, +from .stored_playlists import (StoredPlaylistsController, BaseStoredPlaylistsProvider) logger = logging.getLogger('mopidy.backends.base') @@ -57,7 +57,7 @@ class BaseBackend(object): playback = None #: The stored playlists controller. An instance of - #: :class:`mopidy.backends.base.BaseStoredPlaylistsController`. + #: :class:`mopidy.backends.base.StoredPlaylistsController`. stored_playlists = None #: List of URI prefixes this backend can handle. diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py index bb86c92e..733ea46f 100644 --- a/mopidy/backends/base/stored_playlists.py +++ b/mopidy/backends/base/stored_playlists.py @@ -3,7 +3,7 @@ import logging logger = logging.getLogger('mopidy.backends.base') -class BaseStoredPlaylistsController(object): +class StoredPlaylistsController(object): """ :param backend: backend the controller is a part of :type backend: :class:`BaseBackend` @@ -94,7 +94,7 @@ class BaseStoredPlaylistsController(object): def refresh(self): """ Refresh the stored playlists in - :attr:`mopidy.backends.base.BaseStoredPlaylistsController.playlists`. + :attr:`mopidy.backends.base.StoredPlaylistsController.playlists`. """ return self.provider.refresh() @@ -152,7 +152,7 @@ class BaseStoredPlaylistsProvider(object): def create(self, name): """ - See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.create`. + See :meth:`mopidy.backends.base.StoredPlaylistsController.create`. *MUST be implemented by subclass.* """ @@ -160,7 +160,7 @@ class BaseStoredPlaylistsProvider(object): def delete(self, playlist): """ - See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.delete`. + See :meth:`mopidy.backends.base.StoredPlaylistsController.delete`. *MUST be implemented by subclass.* """ @@ -168,7 +168,7 @@ class BaseStoredPlaylistsProvider(object): def lookup(self, uri): """ - See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.lookup`. + See :meth:`mopidy.backends.base.StoredPlaylistsController.lookup`. *MUST be implemented by subclass.* """ @@ -176,7 +176,7 @@ class BaseStoredPlaylistsProvider(object): def refresh(self): """ - See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.refresh`. + See :meth:`mopidy.backends.base.StoredPlaylistsController.refresh`. *MUST be implemented by subclass.* """ @@ -184,7 +184,7 @@ class BaseStoredPlaylistsProvider(object): def rename(self, playlist, new_name): """ - See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.rename`. + See :meth:`mopidy.backends.base.StoredPlaylistsController.rename`. *MUST be implemented by subclass.* """ @@ -192,7 +192,7 @@ class BaseStoredPlaylistsProvider(object): def save(self, playlist): """ - See :meth:`mopidy.backends.base.BaseStoredPlaylistsController.save`. + See :meth:`mopidy.backends.base.StoredPlaylistsController.save`. *MUST be implemented by subclass.* """ diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 62a04567..9f3b3af1 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -1,6 +1,6 @@ from mopidy.backends.base import (BaseBackend, CurrentPlaylistController, BasePlaybackController, BasePlaybackProvider, LibraryController, - BaseLibraryProvider, BaseStoredPlaylistsController, + BaseLibraryProvider, StoredPlaylistsController, BaseStoredPlaylistsProvider) from mopidy.models import Playlist @@ -37,7 +37,7 @@ class DummyBackend(BaseBackend): provider=playback_provider) stored_playlists_provider = DummyStoredPlaylistsProvider(backend=self) - self.stored_playlists = BaseStoredPlaylistsController(backend=self, + self.stored_playlists = StoredPlaylistsController(backend=self, provider=stored_playlists_provider) self.uri_handlers = [u'dummy:'] diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index 59ff1fde..985a1243 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -2,7 +2,7 @@ import logging from mopidy import settings from mopidy.backends.base import (BaseBackend, CurrentPlaylistController, - LibraryController, BasePlaybackController, BaseStoredPlaylistsController) + LibraryController, BasePlaybackController, StoredPlaylistsController) logger = logging.getLogger('mopidy.backends.libspotify') @@ -52,7 +52,7 @@ class LibspotifyBackend(BaseBackend): stored_playlists_provider = LibspotifyStoredPlaylistsProvider( backend=self) - self.stored_playlists = BaseStoredPlaylistsController(backend=self, + self.stored_playlists = StoredPlaylistsController(backend=self, provider=stored_playlists_provider) self.uri_handlers = [u'spotify:', u'http://open.spotify.com/'] diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index ef42f4c4..44dc727b 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -7,7 +7,7 @@ import shutil from mopidy import settings from mopidy.backends.base import (BaseBackend, CurrentPlaylistController, LibraryController, BaseLibraryProvider, BasePlaybackController, - BasePlaybackProvider, BaseStoredPlaylistsController, + BasePlaybackProvider, StoredPlaylistsController, BaseStoredPlaylistsProvider) from mopidy.models import Playlist, Track, Album from mopidy.utils.process import pickle_connection @@ -43,7 +43,7 @@ class LocalBackend(BaseBackend): provider=playback_provider) stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self) - self.stored_playlists = BaseStoredPlaylistsController(backend=self, + self.stored_playlists = StoredPlaylistsController(backend=self, provider=stored_playlists_provider) self.uri_handlers = [u'file://'] diff --git a/tests/backends/base/stored_playlists.py b/tests/backends/base/stored_playlists.py index 5bcd322c..0ac0b167 100644 --- a/tests/backends/base/stored_playlists.py +++ b/tests/backends/base/stored_playlists.py @@ -8,7 +8,7 @@ from mopidy.models import Playlist from tests import SkipTest, data_folder -class BaseStoredPlaylistsControllerTest(object): +class StoredPlaylistsControllerTest(object): def setUp(self): settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp() settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache') diff --git a/tests/backends/local/stored_playlists_test.py b/tests/backends/local/stored_playlists_test.py index 4db9e1e2..a7d9043f 100644 --- a/tests/backends/local/stored_playlists_test.py +++ b/tests/backends/local/stored_playlists_test.py @@ -16,10 +16,10 @@ from mopidy.utils.path import path_to_uri from tests import data_folder from tests.backends.base.stored_playlists import \ - BaseStoredPlaylistsControllerTest + StoredPlaylistsControllerTest from tests.backends.local import generate_song -class LocalStoredPlaylistsControllerTest(BaseStoredPlaylistsControllerTest, +class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest, unittest.TestCase): backend_class = LocalBackend From 3fe3ed27b9fba656d99facf4b6c8172a55acbb16 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 3 Nov 2010 00:01:32 +0100 Subject: [PATCH 155/235] Document copy method and update musicbrainz field doc --- mopidy/models.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index c45d6769..60569004 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -38,15 +38,29 @@ class ImmutableObject(object): def __ne__(self, other): return not self.__eq__(other) - def copy(self, **kwargs): + def copy(self, **values): + """ + Copy the model with ``field`` updated to new value. + + Examples:: + + # Returns a track with a new name + Track(name='foo').copy(name='bar') + # Return an album with a new number of tracks + Album(num_tracks=2).copy(num_tracks=5) + + :param values: the model field to modify + :type values: dict + :rtype: new instance of the model being copied + """ data = {} for key in self.__dict__.keys(): public_key = key.lstrip('_') - data[public_key] = kwargs.pop(public_key, self.__dict__[key]) - for key in kwargs.keys(): + data[public_key] = values.pop(public_key, self.__dict__[key]) + for key in values.keys(): if hasattr(self, key): - data[key] = kwargs.pop(key) - if kwargs: + data[key] = values.pop(key) + if values: raise TypeError("copy() got an unexpected keyword argument '%s'" % key) return self.__class__(**data) @@ -56,7 +70,7 @@ class Artist(ImmutableObject): :type uri: string :param name: artist name :type name: string - :param musicbrainz_id: musicbrainz id + :param musicbrainz_id: MusicBrainz ID :type musicbrainz_id: string """ @@ -66,7 +80,7 @@ class Artist(ImmutableObject): #: The artist name. Read-only. name = None - #: The musicbrainz id of the artist. Read-only. + #: The MusicBrainz ID of the artist. Read-only. musicbrainz_id = None @@ -80,7 +94,7 @@ class Album(ImmutableObject): :type artists: list of :class:`Artist` :param num_tracks: number of tracks in album :type num_tracks: integer - :param musicbrainz_id: musicbrainz id + :param musicbrainz_id: MusicBrainz ID :type musicbrainz_id: string """ @@ -93,7 +107,7 @@ class Album(ImmutableObject): #: The number of tracks in the album. Read-only. num_tracks = 0 - #: The musicbrainz id of the album. Read-only. + #: The MusicBrainz ID of the album. Read-only. musicbrainz_id = None def __init__(self, *args, **kwargs): @@ -124,7 +138,7 @@ class Track(ImmutableObject): :type length: integer :param bitrate: bitrate in kbit/s :type bitrate: integer - :param musicbrainz_id: musicbrainz id + :param musicbrainz_id: MusicBrainz ID :type musicbrainz_id: string """ @@ -149,7 +163,7 @@ class Track(ImmutableObject): #: The track's bitrate in kbit/s. Read-only. bitrate = None - #: The musicbrainz id of the track. Read-only. + #: The MusicBrainz ID of the track. Read-only. musicbrainz_id = None def __init__(self, *args, **kwargs): From 9a1f16cba6999a2e67293d642a4e192a82dc64e5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 00:03:08 +0100 Subject: [PATCH 156/235] Rename {Base => }Backend --- docs/api/backends/controllers.rst | 2 +- mopidy/backends/base/__init__.py | 2 +- mopidy/backends/base/current_playlist.py | 2 +- mopidy/backends/base/library.py | 4 ++-- mopidy/backends/base/playback.py | 4 ++-- mopidy/backends/base/stored_playlists.py | 4 ++-- mopidy/backends/dummy/__init__.py | 4 ++-- mopidy/backends/libspotify/__init__.py | 4 ++-- mopidy/backends/local/__init__.py | 4 ++-- mopidy/frontends/base.py | 2 +- mopidy/mixers/base.py | 2 +- 11 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/api/backends/controllers.rst b/docs/api/backends/controllers.rst index 97ce2508..a48a472b 100644 --- a/docs/api/backends/controllers.rst +++ b/docs/api/backends/controllers.rst @@ -13,7 +13,7 @@ The backend controller API is the interface that is used by frontends like The backend =========== -.. autoclass:: mopidy.backends.base.BaseBackend +.. autoclass:: mopidy.backends.base.Backend :members: :undoc-members: diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index f1fdfcbe..ae668807 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -16,7 +16,7 @@ from .stored_playlists import (StoredPlaylistsController, logger = logging.getLogger('mopidy.backends.base') -class BaseBackend(object): +class Backend(object): """ :param core_queue: a queue for sending messages to :class:`mopidy.process.CoreProcess` diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index fec8e5a3..fe7d1de9 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -9,7 +9,7 @@ logger = logging.getLogger('mopidy.backends.base') class CurrentPlaylistController(object): """ :param backend: backend the controller is a part of - :type backend: :class:`BaseBackend` + :type backend: :class:`mopidy.backends.base.Backend` """ def __init__(self, backend): diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py index f3477489..fd018b5f 100644 --- a/mopidy/backends/base/library.py +++ b/mopidy/backends/base/library.py @@ -5,7 +5,7 @@ logger = logging.getLogger('mopidy.backends.base') class LibraryController(object): """ :param backend: backend the controller is a part of - :type backend: :class:`BaseBackend` + :type backend: :class:`mopidy.backends.base.Backend` :param provider: provider the controller should use :type provider: instance of :class:`BaseLibraryProvider` """ @@ -79,7 +79,7 @@ class LibraryController(object): class BaseLibraryProvider(object): """ :param backend: backend the controller is a part of - :type backend: :class:`BaseBackend` + :type backend: :class:`mopidy.backends.base.Backend` """ def __init__(self, backend): diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index fa848845..727cce00 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -7,7 +7,7 @@ logger = logging.getLogger('mopidy.backends.base') class BasePlaybackController(object): """ :param backend: the backend - :type backend: :class:`BaseBackend` + :type backend: :class:`mopidy.backends.base.Backend` :param provider: provider the controller should use :type provider: instance of :class:`BasePlaybackProvider` """ @@ -488,7 +488,7 @@ class BasePlaybackController(object): class BasePlaybackProvider(object): """ :param backend: the backend - :type backend: :class:`BaseBackend` + :type backend: :class:`mopidy.backends.base.Backend` """ def __init__(self, backend): diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py index 733ea46f..6578c046 100644 --- a/mopidy/backends/base/stored_playlists.py +++ b/mopidy/backends/base/stored_playlists.py @@ -6,7 +6,7 @@ logger = logging.getLogger('mopidy.backends.base') class StoredPlaylistsController(object): """ :param backend: backend the controller is a part of - :type backend: :class:`BaseBackend` + :type backend: :class:`mopidy.backends.base.Backend` :param provider: provider the controller should use :type provider: instance of :class:`BaseStoredPlaylistsProvider` """ @@ -122,7 +122,7 @@ class StoredPlaylistsController(object): class BaseStoredPlaylistsProvider(object): """ :param backend: backend the controller is a part of - :type backend: :class:`BaseBackend` + :type backend: :class:`mopidy.backends.base.Backend` """ def __init__(self, backend): diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 9f3b3af1..5106de14 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -1,4 +1,4 @@ -from mopidy.backends.base import (BaseBackend, CurrentPlaylistController, +from mopidy.backends.base import (Backend, CurrentPlaylistController, BasePlaybackController, BasePlaybackProvider, LibraryController, BaseLibraryProvider, StoredPlaylistsController, BaseStoredPlaylistsProvider) @@ -13,7 +13,7 @@ class DummyQueue(object): self.received_messages.append(message) -class DummyBackend(BaseBackend): +class DummyBackend(Backend): """ A backend which implements the backend API in the simplest way possible. Used in tests of the frontends. diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index 985a1243..00619a6c 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -1,14 +1,14 @@ import logging from mopidy import settings -from mopidy.backends.base import (BaseBackend, CurrentPlaylistController, +from mopidy.backends.base import (Backend, CurrentPlaylistController, LibraryController, BasePlaybackController, StoredPlaylistsController) logger = logging.getLogger('mopidy.backends.libspotify') ENCODING = 'utf-8' -class LibspotifyBackend(BaseBackend): +class LibspotifyBackend(Backend): """ A `Spotify `_ backend which uses the official `libspotify `_ diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 44dc727b..68654749 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -5,7 +5,7 @@ import os import shutil from mopidy import settings -from mopidy.backends.base import (BaseBackend, CurrentPlaylistController, +from mopidy.backends.base import (Backend, CurrentPlaylistController, LibraryController, BaseLibraryProvider, BasePlaybackController, BasePlaybackProvider, StoredPlaylistsController, BaseStoredPlaylistsProvider) @@ -16,7 +16,7 @@ from .translator import parse_m3u, parse_mpd_tag_cache logger = logging.getLogger(u'mopidy.backends.local') -class LocalBackend(BaseBackend): +class LocalBackend(Backend): """ A backend for playing music from a local music archive. diff --git a/mopidy/frontends/base.py b/mopidy/frontends/base.py index e15c4745..bf1c9bda 100644 --- a/mopidy/frontends/base.py +++ b/mopidy/frontends/base.py @@ -5,7 +5,7 @@ class BaseFrontend(object): :param core_queue: queue for messaging the core :type core_queue: :class:`multiprocessing.Queue` :param backend: the backend - :type backend: :class:`mopidy.backends.base.BaseBackend` + :type backend: :class:`mopidy.backends.base.Backend` """ def __init__(self, core_queue, backend): diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py index 231154ee..f7f9525c 100644 --- a/mopidy/mixers/base.py +++ b/mopidy/mixers/base.py @@ -3,7 +3,7 @@ from mopidy import settings class BaseMixer(object): """ :param backend: a backend instance - :type mixer: :class:`mopidy.backends.base.BaseBackend` + :type backend: :class:`mopidy.backends.base.Backend` **Settings:** From fc9daa5a5a95df0ce2b9fadc2cc989cea9b9f120 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 00:06:32 +0100 Subject: [PATCH 157/235] Rename {Base => }PlaybackController --- docs/api/backends/controllers.rst | 2 +- mopidy/backends/base/__init__.py | 4 ++-- mopidy/backends/base/playback.py | 2 +- mopidy/backends/dummy/__init__.py | 4 ++-- mopidy/backends/libspotify/__init__.py | 4 ++-- mopidy/backends/local/__init__.py | 4 ++-- tests/backends/base/playback.py | 2 +- tests/backends/local/playback_test.py | 6 ++---- 8 files changed, 13 insertions(+), 15 deletions(-) diff --git a/docs/api/backends/controllers.rst b/docs/api/backends/controllers.rst index a48a472b..88bb48ff 100644 --- a/docs/api/backends/controllers.rst +++ b/docs/api/backends/controllers.rst @@ -24,7 +24,7 @@ Playback controller Manages playback, with actions like play, pause, stop, next, previous, and seek. -.. autoclass:: mopidy.backends.base.BasePlaybackController +.. autoclass:: mopidy.backends.base.PlaybackController :members: :undoc-members: diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index ae668807..096a433f 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -10,7 +10,7 @@ from mopidy.utils import get_class from .current_playlist import CurrentPlaylistController from .library import LibraryController, BaseLibraryProvider -from .playback import BasePlaybackController, BasePlaybackProvider +from .playback import PlaybackController, BasePlaybackProvider from .stored_playlists import (StoredPlaylistsController, BaseStoredPlaylistsProvider) @@ -53,7 +53,7 @@ class Backend(object): mixer = None #: The playback controller. An instance of - #: :class:`mopidy.backends.base.BasePlaybackController`. + #: :class:`mopidy.backends.base.PlaybackController`. playback = None #: The stored playlists controller. An instance of diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 727cce00..8a3eeee5 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -4,7 +4,7 @@ import time logger = logging.getLogger('mopidy.backends.base') -class BasePlaybackController(object): +class PlaybackController(object): """ :param backend: the backend :type backend: :class:`mopidy.backends.base.Backend` diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 5106de14..9c6885bc 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -1,5 +1,5 @@ from mopidy.backends.base import (Backend, CurrentPlaylistController, - BasePlaybackController, BasePlaybackProvider, LibraryController, + PlaybackController, BasePlaybackProvider, LibraryController, BaseLibraryProvider, StoredPlaylistsController, BaseStoredPlaylistsProvider) from mopidy.models import Playlist @@ -33,7 +33,7 @@ class DummyBackend(Backend): provider=library_provider) playback_provider = DummyPlaybackProvider(backend=self) - self.playback = BasePlaybackController(backend=self, + self.playback = PlaybackController(backend=self, provider=playback_provider) stored_playlists_provider = DummyStoredPlaylistsProvider(backend=self) diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index 00619a6c..4d8b67d5 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -2,7 +2,7 @@ import logging from mopidy import settings from mopidy.backends.base import (Backend, CurrentPlaylistController, - LibraryController, BasePlaybackController, StoredPlaylistsController) + LibraryController, PlaybackController, StoredPlaylistsController) logger = logging.getLogger('mopidy.backends.libspotify') @@ -47,7 +47,7 @@ class LibspotifyBackend(Backend): provider=library_provider) playback_provider = LibspotifyPlaybackProvider(backend=self) - self.playback = BasePlaybackController(backend=self, + self.playback = PlaybackController(backend=self, provider=playback_provider) stored_playlists_provider = LibspotifyStoredPlaylistsProvider( diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 68654749..532c3976 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -6,7 +6,7 @@ import shutil from mopidy import settings from mopidy.backends.base import (Backend, CurrentPlaylistController, - LibraryController, BaseLibraryProvider, BasePlaybackController, + LibraryController, BaseLibraryProvider, PlaybackController, BasePlaybackProvider, StoredPlaylistsController, BaseStoredPlaylistsProvider) from mopidy.models import Playlist, Track, Album @@ -49,7 +49,7 @@ class LocalBackend(Backend): self.uri_handlers = [u'file://'] -class LocalPlaybackController(BasePlaybackController): +class LocalPlaybackController(PlaybackController): def __init__(self, *args, **kwargs): super(LocalPlaybackController, self).__init__(*args, **kwargs) diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index 7b6efe7a..26662f96 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -13,7 +13,7 @@ from tests.backends.base import populate_playlist # TODO Test 'playlist repeat', e.g. repeat=1,single=0 -class BasePlaybackControllerTest(object): +class PlaybackControllerTest(object): tracks = [] def setUp(self): diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py index a84dfcde..2007cff8 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -12,12 +12,10 @@ from mopidy.models import Track from mopidy.utils.path import path_to_uri from tests import data_folder -from tests.backends.base.playback import BasePlaybackControllerTest +from tests.backends.base.playback import PlaybackControllerTest from tests.backends.local import generate_song -class LocalPlaybackControllerTest(BasePlaybackControllerTest, - unittest.TestCase): - +class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase): backend_class = LocalBackend tracks = [Track(uri=generate_song(i), length=4464) for i in range(1, 4)] From b234c0553c3fb219d05b0fc3073e332c7d2e038f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 00:09:13 +0100 Subject: [PATCH 158/235] Fix docstring indentation --- mopidy/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index 60569004..915dc92a 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -49,9 +49,9 @@ class ImmutableObject(object): # Return an album with a new number of tracks Album(num_tracks=2).copy(num_tracks=5) - :param values: the model field to modify - :type values: dict - :rtype: new instance of the model being copied + :param values: the model fields to modify + :type values: dict + :rtype: new instance of the model being copied """ data = {} for key in self.__dict__.keys(): From 1ed29a4b9a65b6e971ad191b0bc1b91795bee136 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 00:11:46 +0100 Subject: [PATCH 159/235] Fix pylint warnings --- mopidy/frontends/mpd/translator.py | 6 ++++-- mopidy/models.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 099b9551..3ead23c7 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -46,9 +46,11 @@ def track_to_mpd_format(track, position=None, cpid=None): # FIXME don't use first and best artist? # FIXME don't duplicate following code? if track.album is not None and track.album.artists: - artists = filter(lambda a: a.musicbrainz_id is not None, track.album.artists) + artists = filter(lambda a: a.musicbrainz_id is not None, + track.album.artists) if artists: - result.append(('MUSICBRAINZ_ALBUMARTISTID', artists[0].musicbrainz_id)) + result.append( + ('MUSICBRAINZ_ALBUMARTISTID', artists[0].musicbrainz_id)) if track.artists: artists = filter(lambda a: a.musicbrainz_id is not None, track.artists) if artists: diff --git a/mopidy/models.py b/mopidy/models.py index 915dc92a..8e7585f1 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -61,7 +61,8 @@ class ImmutableObject(object): if hasattr(self, key): data[key] = values.pop(key) if values: - raise TypeError("copy() got an unexpected keyword argument '%s'" % key) + raise TypeError("copy() got an unexpected keyword argument '%s'" + % key) return self.__class__(**data) class Artist(ImmutableObject): From 17c30c8bcd59526d5ed199bf851e0b3626896e56 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 00:17:53 +0100 Subject: [PATCH 160/235] docs: Update refs to BaseMixer --- docs/api/backends/controllers.rst | 2 +- docs/api/mixers.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/backends/controllers.rst b/docs/api/backends/controllers.rst index 88bb48ff..28112cf7 100644 --- a/docs/api/backends/controllers.rst +++ b/docs/api/backends/controllers.rst @@ -32,7 +32,7 @@ seek. Mixer controller ================ -Manages volume. See :class:`mopidy.mixers.BaseMixer`. +Manages volume. See :class:`mopidy.mixers.base.BaseMixer`. Current playlist controller diff --git a/docs/api/mixers.rst b/docs/api/mixers.rst index 434637f3..6daa7a4e 100644 --- a/docs/api/mixers.rst +++ b/docs/api/mixers.rst @@ -24,10 +24,10 @@ enable one of the hardware device mixers, you must the set :attr:`mopidy.settings.MIXER` setting to point to one of the classes found below, and possibly add some extra settings required by the mixer you choose. -All mixers should subclass :class:`mopidy.mixers.BaseMixer` and override +All mixers should subclass :class:`mopidy.mixers.base.BaseMixer` and override methods as described below. -.. automodule:: mopidy.mixers +.. automodule:: mopidy.mixers.base :synopsis: Mixer API :members: :undoc-members: From 95aa739c96ede3fb5a2a4c6486fb2eb14eef6e56 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 00:35:27 +0100 Subject: [PATCH 161/235] Update changelog --- docs/api/backends/concepts.rst | 2 ++ docs/changes.rst | 43 ++++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/docs/api/backends/concepts.rst b/docs/api/backends/concepts.rst index fd7b4d13..0d476213 100644 --- a/docs/api/backends/concepts.rst +++ b/docs/api/backends/concepts.rst @@ -1,3 +1,5 @@ +.. _backend-concepts: + ********************************************** The backend, controller, and provider concepts ********************************************** diff --git a/docs/changes.rst b/docs/changes.rst index c3df7d85..add4cfc6 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,10 +12,45 @@ No description yet. **Changes** -- Install ``mopidy.desktop`` file that makes Mopidy available from e.g. Gnome - application menus. -- Add :command:`mopidy-scan` command to generate ``tag_cache`` files without - any help from the original MPD server. +- Packaging and distribution: + + - Install ``mopidy.desktop`` file that makes Mopidy available from e.g. Gnome + application menus. + - Create infrastructure for creating Debian packages of Mopidy. + +- Local backend: + + - Add :command:`mopidy-scan` command to generate ``tag_cache`` files without + any help from the original MPD server. + +- Models: + + - Rename and generalize ``Playlist._with(**kwargs)`` to + :meth:`mopidy.models.ImmutableObject.copy`. + +- Introduce the :ref:`provider concept `. Split the backend + API into a :ref:`backend controller API ` (for + frontend use) and a :ref:`backend provider API ` (for + backend implementation use), which includes the following changes: + + - Rename ``BaseBackend`` to :class:`mopidy.backends.base.Backend`. + - Rename ``BaseCurrentPlaylistController`` to + :class:`mopidy.backends.base.CurrentPlaylistController`. + - Split ``BaseLibraryController`` to + :class:`mopidy.backends.base.LibraryController` and + :class:`mopidy.backends.base.BaseLibraryProvider`. + - Split ``BasePlaybackController`` to + :class:`mopidy.backends.base.PlaybackController` and + :class:`mopidy.backends.base.BasePlaybackProvider`. + - Split ``BaseStoredPlaylistsController`` to + :class:`mopidy.backends.base.StoredPlaylistsController` and + :class:`mopidy.backends.base.BaseStoredPlaylistsProvider`. + +- Other API and package structure cleaning: + + - Move ``BaseMixer`` to :class:`mopidy.mixers.base.BaseMixer`. + - Add docs for the current non-stable output API, + :class:`mopidy.outputs.base.BaseOutput`. 0.2.0 (2010-10-24) From e4f006030b52b976bd51d2dd7312df9afb0b51d7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 00:45:48 +0100 Subject: [PATCH 162/235] Update changelog --- docs/changes.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index add4cfc6..6c9f99f1 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,6 +12,19 @@ No description yet. **Changes** +- Settings: + + - Automatically expand ``~`` to the user's home directory in settings with + names ending in ``_PATH`` or ``_FILE``. + - Rename the following settings. The settings validator will warn you if you + need to change your local settings. + + - ``LOCAL_MUSIC_FOLDER`` to :attr:`mopidy.settings.LOCAL_MUSIC_PATH` + - ``LOCAL_PLAYLIST_FOLDER`` to + :attr:`mopidy.settings.LOCAL_PLAYLIST_PATH` + - ``LOCAL_TAG_CACHE`` to :attr:`mopidy.settings.LOCAL_TAG_CACHE_PATH` + - ``SPOTIFY_LIB_CACHE`` to :attr:`mopidy.settings.SPOTIFY_CACHE_PATH` + - Packaging and distribution: - Install ``mopidy.desktop`` file that makes Mopidy available from e.g. Gnome @@ -22,11 +35,14 @@ No description yet. - Add :command:`mopidy-scan` command to generate ``tag_cache`` files without any help from the original MPD server. + - Support UTF-8 encoded tag caches with non-ASCII characters. - Models: - Rename and generalize ``Playlist._with(**kwargs)`` to :meth:`mopidy.models.ImmutableObject.copy`. + - Add ``musicbrainz_id`` field to :class:`mopidy.models.Artist`, + :class:`mopidy.models.Album`, and :class:`mopidy.models.Track`. - Introduce the :ref:`provider concept `. Split the backend API into a :ref:`backend controller API ` (for From a44a86acc8f28070bef2e1a84452ec4529f77b75 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 00:51:36 +0100 Subject: [PATCH 163/235] Update changelog --- docs/changes.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 6c9f99f1..d957e608 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -14,15 +14,15 @@ No description yet. - Settings: - - Automatically expand ``~`` to the user's home directory in settings with - names ending in ``_PATH`` or ``_FILE``. + - Automatically expand ``~`` to the user's home directory and make the path + absolute for settings with names ending in ``_PATH`` or ``_FILE``. - Rename the following settings. The settings validator will warn you if you need to change your local settings. - ``LOCAL_MUSIC_FOLDER`` to :attr:`mopidy.settings.LOCAL_MUSIC_PATH` - ``LOCAL_PLAYLIST_FOLDER`` to :attr:`mopidy.settings.LOCAL_PLAYLIST_PATH` - - ``LOCAL_TAG_CACHE`` to :attr:`mopidy.settings.LOCAL_TAG_CACHE_PATH` + - ``LOCAL_TAG_CACHE`` to :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE` - ``SPOTIFY_LIB_CACHE`` to :attr:`mopidy.settings.SPOTIFY_CACHE_PATH` - Packaging and distribution: From 8b5c3cfadc75db6ca12947e5c836bb36ba22bf49 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Nov 2010 23:51:12 +0100 Subject: [PATCH 164/235] docs: Update donations section --- docs/authors.rst | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/authors.rst b/docs/authors.rst index f56242a5..01e810e4 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -10,13 +10,20 @@ Contributors to Mopidy in the order of appearance: - Kristian Klette -Donations -========= +Showing your appreciation +========================= If you already enjoy Mopidy, or don't enjoy it and want to help us making -Mopidy better, you can `donate money `_ to -Mopidy's development. +Mopidy better, the best way to do so is to contribute back to the community. +You can contribute code, documentation, tests, bug reports, or help other +users, spreading the word, etc. + +If you want to show your appreciation in a less time consuming way, you can +`flattr us `_, or `donate money +`_ to Mopidy's development. + +We promise that any money donated -- to Pledgie, not Flattr, due to the size of +the amounts -- will be used to cover costs related to Mopidy development, like +service subscriptions (Spotify, Last.fm, etc.) and hardware devices like an +used iPod Touch for testing Mopidy with MPod. -Any donated money will be used to cover service subscriptions (e.g. Spotify -and Last.fm) and hardware devices (e.g. an used iPod Touch for testing Mopidy -with MPod) needed for developing Mopidy. From 97f31d3cadb4d6b1de10ba27145f01e21a7c88ad Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 19 Nov 2010 22:06:41 +0100 Subject: [PATCH 165/235] docs: Fix module headers --- docs/modules/frontends/lastfm.rst | 6 +++--- docs/modules/frontends/mpd.rst | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/modules/frontends/lastfm.rst b/docs/modules/frontends/lastfm.rst index bd3e218e..a726f4a2 100644 --- a/docs/modules/frontends/lastfm.rst +++ b/docs/modules/frontends/lastfm.rst @@ -1,6 +1,6 @@ -****************************** -:mod:`mopidy.frontends.lastfm` -****************************** +*************************************************** +:mod:`mopidy.frontends.lastfm` -- Last.fm Scrobbler +*************************************************** .. automodule:: mopidy.frontends.lastfm :synopsis: Last.fm scrobbler frontend diff --git a/docs/modules/frontends/mpd.rst b/docs/modules/frontends/mpd.rst index 6361e909..35128e70 100644 --- a/docs/modules/frontends/mpd.rst +++ b/docs/modules/frontends/mpd.rst @@ -1,6 +1,6 @@ -*************************** -:mod:`mopidy.frontends.mpd` -*************************** +***************************************** +:mod:`mopidy.frontends.mpd` -- MPD server +***************************************** .. automodule:: mopidy.frontends.mpd :synopsis: MPD frontend From f307f86620874abfcf37ab457a5b7f1efd23578b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 19 Nov 2010 22:38:19 +0100 Subject: [PATCH 166/235] docs: Update roadmap --- docs/development/roadmap.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst index 2b0cf2ba..175c62ac 100644 --- a/docs/development/roadmap.rst +++ b/docs/development/roadmap.rst @@ -27,11 +27,11 @@ Possible targets for the next version - Write-support for Spotify, i.e. playlist management. - Virtual directories with e.g. starred tracks from Spotify. - - Support for 320 kbps audio. + - **[WIP: possibly v0.3]** Support for 320 kbps audio. - Local backend: - - Better library support. + - Better music library support. - **[DONE: v0.3]** A script for creating a tag cache. - An alternative to tag cache for caching metadata, i.e. Sqlite. @@ -43,7 +43,7 @@ Stuff we want to do, but not right now, and maybe never - Packaging and distribution: - - **[PENDING]** Create `Homebrew `_ + - **[BLOCKED]** Create `Homebrew `_ recipies for all our dependencies and Mopidy itself to make OS X installation a breeze. See `Homebrew's issue #1612 `_. @@ -68,8 +68,9 @@ Stuff we want to do, but not right now, and maybe never - Publish the server's presence to the network using `Zeroconf `_/Avahi. - **[WIP: feature/mpris-frontend]** D-Bus/`MPRIS `_ - - REST/JSON web service with a jQuery client as example application. Maybe - based upon `Tornado `_ and `jQuery + - **[WIP: feature/http-frontend]** REST/JSON web service with a jQuery client + as example application. Maybe based upon `Tornado + `_ and `jQuery Mobile `_. - DNLA/UPnP so Mopidy can be controlled from i.e. TVs. - `XMMS2 `_ From 133a22ef978108fdfd61706558729fee7c2a5925 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Tue, 23 Nov 2010 00:26:23 +0100 Subject: [PATCH 167/235] add SPOTIFY_HIGH_BITRATE setting to enable streaming of Spotify's high bitrate streams --- mopidy/backends/libspotify/session_manager.py | 6 ++++++ mopidy/settings.py | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/libspotify/session_manager.py index 5831b713..e70e8cfa 100644 --- a/mopidy/backends/libspotify/session_manager.py +++ b/mopidy/backends/libspotify/session_manager.py @@ -36,6 +36,12 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread): logger.info(u'Connected to Spotify') self.session = session self.connected.set() + if settings.SPOTIFY_HIGH_BITRATE: + logger.debug(u'Prefer high bitrate') + self.session.set_preferred_bitrate(1) + else: + logger.debug(u'Prefer normal bitrate') + self.session.set_preferred_bitrate(0) def logged_out(self, session): """Callback used by pyspotify""" diff --git a/mopidy/settings.py b/mopidy/settings.py index 4f60ee99..dc315064 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -183,3 +183,8 @@ SPOTIFY_USERNAME = u'' #: #: Used by :mod:`mopidy.backends.libspotify`. SPOTIFY_PASSWORD = u'' + +#: Do you prefer high bitrate (320k)? +#: +#: Used by :mod:`mopidy.backends.libspotify`. +SPOTIFY_HIGH_BITRATE = False From 975b264c2d70797184c806f65b03cf3549448895 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Tue, 23 Nov 2010 00:33:14 +0100 Subject: [PATCH 168/235] set connection connected after preferred bitrate is set --- mopidy/backends/libspotify/session_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/libspotify/session_manager.py index e70e8cfa..8a79088f 100644 --- a/mopidy/backends/libspotify/session_manager.py +++ b/mopidy/backends/libspotify/session_manager.py @@ -35,13 +35,13 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread): """Callback used by pyspotify""" logger.info(u'Connected to Spotify') self.session = session - self.connected.set() if settings.SPOTIFY_HIGH_BITRATE: logger.debug(u'Prefer high bitrate') self.session.set_preferred_bitrate(1) else: logger.debug(u'Prefer normal bitrate') self.session.set_preferred_bitrate(0) + self.connected.set() def logged_out(self, session): """Callback used by pyspotify""" From 73ed9920c43d741751ba5c17318d94c5fd1bffe5 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Tue, 23 Nov 2010 00:36:02 +0100 Subject: [PATCH 169/235] Document the default value on the Spotify preferred bitrate setting --- mopidy/settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mopidy/settings.py b/mopidy/settings.py index dc315064..1aaa4318 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -187,4 +187,8 @@ SPOTIFY_PASSWORD = u'' #: Do you prefer high bitrate (320k)? #: #: Used by :mod:`mopidy.backends.libspotify`. +# +#: Default:: +#: +#: SPOTIFY_HIGH_BITRATE = False # 160k SPOTIFY_HIGH_BITRATE = False From 9cfd1679795cff6cef83bd074d2f83d87f2df40f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 23 Nov 2010 00:56:10 +0100 Subject: [PATCH 170/235] Require upgrade of libspotify to 0.0.6, and pyspotify from the mopidy branch --- docs/changes.rst | 11 +++++++++++ docs/installation/libspotify.rst | 18 ++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index d957e608..3a030145 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -10,6 +10,12 @@ This change log is used to track all major changes to Mopidy. No description yet. +**Important changes** + +- If you use the Spotify backend, you need to upgrade to libspotify 0.0.6 and + the latest pyspotify from the Mopidy developers. Follow the instructions at + :ref:`/installation/libspotify/`. + **Changes** - Settings: @@ -31,6 +37,11 @@ No description yet. application menus. - Create infrastructure for creating Debian packages of Mopidy. +- Spotify backend: + + - Support high bitrate (320k). See + :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE` for details. + - Local backend: - Add :command:`mopidy-scan` command to generate ``tag_cache`` files without diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index b3ea06fa..9dc91066 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -5,7 +5,7 @@ libspotify installation Mopidy uses `libspotify `_ for playing music from the Spotify music service. To use :mod:`mopidy.backends.libspotify` you must -install libspotify and `pyspotify `_. +install libspotify and `pyspotify `_. .. warning:: @@ -22,14 +22,14 @@ install libspotify and `pyspotify `_. Installing libspotify on Linux ============================== -Download and install libspotify 0.0.4 for your OS and CPU architecture from +Download and install libspotify 0.0.6 for your OS and CPU architecture from https://developer.spotify.com/en/libspotify/. For 64-bit Linux the process is as follows:: - wget http://developer.spotify.com/download/libspotify/libspotify-0.0.4-linux6-x86_64.tar.gz - tar zxfv libspotify-0.0.4-linux6-x86_64.tar.gz - cd libspotify-0.0.4-linux6-x86_64/ + wget http://developer.spotify.com/download/libspotify/libspotify-0.0.6-linux6-x86_64.tar.gz + tar zxfv libspotify-0.0.6-linux6-x86_64.tar.gz + cd libspotify-0.0.6-linux6-x86_64/ sudo make install prefix=/usr/local sudo ldconfig @@ -46,6 +46,11 @@ libspotify:: brew install libspotify +To update your existing libspotify installation using Homebrew:: + + brew update + brew install `brew outdated` + When libspotify has been installed, continue with :ref:`pyspotify_installation`. @@ -69,7 +74,8 @@ In OS X no additional dependencies are needed. Check out the pyspotify code, and install it:: - git clone git://github.com/jodal/pyspotify.git + wget --no-check-certificate -O pyspotify.tar.gz https://github.com/mopidy/pyspotify/tarball/mopidy + tar zxfv pyspotify.tar.gz cd pyspotify/pyspotify/ sudo rm -rf build/ # If you are upgrading pyspotify sudo python setup.py install From 1c48c7ab2f758836358bd8b05f2cebec76ce49ce Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 23 Nov 2010 01:31:49 +0100 Subject: [PATCH 171/235] Show 320kbit bitrate in MPD if SPOTIFY_HIGH_BITRATE=True --- mopidy/backends/libspotify/translator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/libspotify/translator.py b/mopidy/backends/libspotify/translator.py index ff8f3c5c..09303eda 100644 --- a/mopidy/backends/libspotify/translator.py +++ b/mopidy/backends/libspotify/translator.py @@ -2,8 +2,9 @@ import datetime as dt from spotify import Link -from mopidy.models import Artist, Album, Track, Playlist +from mopidy import settings from mopidy.backends.libspotify import ENCODING +from mopidy.models import Artist, Album, Track, Playlist class LibspotifyTranslator(object): @classmethod @@ -39,7 +40,7 @@ class LibspotifyTranslator(object): track_no=spotify_track.index(), date=date, length=spotify_track.duration(), - bitrate=160, + bitrate=(settings.SPOTIFY_HIGH_BITRATE and 320 or 160), ) @classmethod From 0b9d7bbd3f3a172d6d5acf83184568c074088fe9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 23 Nov 2010 09:01:44 +0100 Subject: [PATCH 172/235] Switch main GitHub repo from jodal/mopidy to mopidy/mopidy --- README.rst | 6 +++--- docs/changes.rst | 2 +- docs/conf.py | 2 +- docs/installation/index.rst | 2 +- mopidy/backends/libspotify/__init__.py | 3 ++- mopidy/backends/local/__init__.py | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index c6187119..4f31fb59 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,7 @@ To install Mopidy, check out * `Documentation (latest release) `_ * `Documentation (development version) `_ -* `Source code `_ -* `Issue tracker `_ +* `Source code `_ +* `Issue tracker `_ * IRC: ``#mopidy`` at `irc.freenode.net `_ -* `Download development snapshot `_ +* `Download development snapshot `_ diff --git a/docs/changes.rst b/docs/changes.rst index 3a030145..1c97ed68 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -426,7 +426,7 @@ Mopidy is working and usable. 0.1.0a0 is an alpha release, which basicly means we will still change APIs, add features, etc. before the final 0.1.0 release. But the software is usable as is, so we release it. Please give it a try and give us feedback, either at our IRC channel or through the `issue tracker -`_. Thanks! +`_. Thanks! **Changes** diff --git a/docs/conf.py b/docs/conf.py index 16a85975..9e7ff1fb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -202,4 +202,4 @@ latex_documents = [ needs_sphinx = '1.0' -extlinks = {'issue': ('http://github.com/jodal/mopidy/issues#issue/%s', 'GH-')} +extlinks = {'issue': ('http://github.com/mopidy/mopidy/issues#issue/%s', 'GH-')} diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 580ecd6d..3f0600f4 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -103,7 +103,7 @@ ckeckout:: sudo aptitude install git-core # On Ubuntu/Debian sudo brew install git # On OS X - git clone git://github.com/jodal/mopidy.git + git clone git://github.com/mopidy/mopidy.git cd mopidy/ python mopidy # Yes, 'mopidy' is a dir diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index 4d8b67d5..ad2926c7 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -15,7 +15,8 @@ class LibspotifyBackend(Backend): library and the `pyspotify `_ Python bindings for libspotify. - **Issues:** http://github.com/jodal/mopidy/issues/labels/backend-libspotify + **Issues:** + http://github.com/mopidy/mopidy/issues/labels/backend-libspotify **Settings:** diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 532c3976..e3e1d5dc 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -20,7 +20,7 @@ class LocalBackend(Backend): """ A backend for playing music from a local music archive. - **Issues:** http://github.com/jodal/mopidy/issues/labels/backend-local + **Issues:** http://github.com/mopidy/mopidy/issues/labels/backend-local **Settings:** From 827940a97785af2c8232c3a1997d90909be4c7a9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 23 Nov 2010 09:39:40 +0100 Subject: [PATCH 173/235] Update Git repo references in tests --- tests/frontends/mpd/regression_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/frontends/mpd/regression_test.py b/tests/frontends/mpd/regression_test.py index 3cfdb855..7e7163d8 100644 --- a/tests/frontends/mpd/regression_test.py +++ b/tests/frontends/mpd/regression_test.py @@ -8,7 +8,7 @@ from mopidy.models import Track class IssueGH17RegressionTest(unittest.TestCase): """ - The issue: http://github.com/jodal/mopidy/issues#issue/17 + The issue: http://github.com/mopidy/mopidy/issues#issue/17 How to reproduce: @@ -42,7 +42,7 @@ class IssueGH17RegressionTest(unittest.TestCase): class IssueGH18RegressionTest(unittest.TestCase): """ - The issue: http://github.com/jodal/mopidy/issues#issue/18 + The issue: http://github.com/mopidy/mopidy/issues#issue/18 How to reproduce: @@ -79,7 +79,7 @@ class IssueGH18RegressionTest(unittest.TestCase): class IssueGH22RegressionTest(unittest.TestCase): """ - The issue: http://github.com/jodal/mopidy/issues/#issue/22 + The issue: http://github.com/mopidy/mopidy/issues/#issue/22 How to reproduce: From 6d21f86a9e351abbae6fe9fb1b030ec6a4fa734c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 28 Dec 2010 14:27:09 +0100 Subject: [PATCH 174/235] Fix broken link --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 1c97ed68..bd4f08ae 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -14,7 +14,7 @@ No description yet. - If you use the Spotify backend, you need to upgrade to libspotify 0.0.6 and the latest pyspotify from the Mopidy developers. Follow the instructions at - :ref:`/installation/libspotify/`. + :doc:`/installation/libspotify/`. **Changes** From f7b4f65e31640393bda643999e0e67f1e390b195 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 28 Dec 2010 14:30:36 +0100 Subject: [PATCH 175/235] Move 320k support to the important section of the changelog --- docs/changes.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index bd4f08ae..7bf1ebba 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,9 +12,15 @@ No description yet. **Important changes** -- If you use the Spotify backend, you need to upgrade to libspotify 0.0.6 and - the latest pyspotify from the Mopidy developers. Follow the instructions at - :doc:`/installation/libspotify/`. +- Spotify backend: + + - If you use the Spotify backend, you need to upgrade to libspotify 0.0.6 and + the latest pyspotify from the Mopidy developers. Follow the instructions at + :doc:`/installation/libspotify/`. + + - Support high bitrate (320k) audio. See + :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE` for details. + **Changes** @@ -37,11 +43,6 @@ No description yet. application menus. - Create infrastructure for creating Debian packages of Mopidy. -- Spotify backend: - - - Support high bitrate (320k). See - :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE` for details. - - Local backend: - Add :command:`mopidy-scan` command to generate ``tag_cache`` files without From 2379e772c7c39b47c314c774d4b59aff4d4a56fe Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 28 Dec 2010 14:34:34 +0100 Subject: [PATCH 176/235] Remove warning that is not needed when we don't pull pyspotify from Git --- docs/installation/libspotify.rst | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index 9dc91066..692bda36 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -77,12 +77,4 @@ Check out the pyspotify code, and install it:: wget --no-check-certificate -O pyspotify.tar.gz https://github.com/mopidy/pyspotify/tarball/mopidy tar zxfv pyspotify.tar.gz cd pyspotify/pyspotify/ - sudo rm -rf build/ # If you are upgrading pyspotify sudo python setup.py install - -.. note:: - - The ``sudo rm -rf build/`` step is needed if you are upgrading pyspotify. - Simply running ``python setup.py clean`` will *not* clean out the C parts - of the ``build/`` directory, and you will thus not get any changes to the C - code included in your installation. From 62af31cbcae57fa2717942a86f0c63ab347fd041 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 28 Dec 2010 15:00:23 +0100 Subject: [PATCH 177/235] Doc how to install dev snapshot using pip --- docs/installation/index.rst | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 3f0600f4..96ba6027 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -61,18 +61,16 @@ Make sure you got the required dependencies installed. - pylast >= 4.3.0 -Install latest release -====================== +Install latest stable release +============================= -To install the currently latest release of Mopidy using ``pip``:: +To install the currently latest stable release of Mopidy using ``pip``:: sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian sudo brew install pip # On OS X - sudo pip install mopidy + sudo pip install -U Mopidy -To later upgrade to the latest release:: - - sudo pip install -U mopidy +To later upgrade to the latest release, just rerun the last command. If you for some reason can't use ``pip``, try ``easy_install``. @@ -94,20 +92,21 @@ Next, you need to set a couple of :doc:`settings `, and then you're ready to :doc:`run Mopidy `. -Run from source code checkout -============================= +Track development using Git +=========================== -If you may want to contribute to Mopidy, and want access to other branches as -well, you can checkout the Mopidy source from Git and run it directly from the -ckeckout:: +If you want to contribute to Mopidy, you should install Mopidy using Git:: sudo aptitude install git-core # On Ubuntu/Debian sudo brew install git # On OS X git clone git://github.com/mopidy/mopidy.git - cd mopidy/ - python mopidy # Yes, 'mopidy' is a dir -To later update to the very latest version:: +You can then run Mopidy directly from the Git repository:: + + cd mopidy/ # Move into the Git repo dir + python mopidy # Run python on the mopidy source code dir + +To get the latest changes to Mopidy:: cd mopidy/ git pull From 18caae4b18a4e20d58aad8329b64be00ead94e9a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 28 Dec 2010 15:12:04 +0100 Subject: [PATCH 178/235] Simplify dependency list --- docs/installation/index.rst | 40 ++++++++++--------------------------- mopidy/mixers/alsa.py | 4 ++++ mopidy/mixers/osa.py | 13 +++++++++++- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 96ba6027..142c5d84 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -20,45 +20,25 @@ Install dependencies Make sure you got the required dependencies installed. - Python >= 2.6, < 3 -- :doc:`GStreamer ` >= 0.10, with Python bindings -- Dependencies for at least one Mopidy mixer: - - :mod:`mopidy.mixers.alsa` (Linux only) +- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`. - - pyalsaaudio >= 0.2 (Debian/Ubuntu package: python-alsaaudio) - - - :mod:`mopidy.mixers.denon` (Linux, OS X, and Windows) - - - pyserial (Debian/Ubuntu package: python-serial) - - - *Default:* :mod:`mopidy.mixers.gstreamer_software` (Linux, OS X, and - Windows) - - - No additional dependencies. - - - :mod:`mopidy.mixers.nad` (Linux, OS X, and Windows) - - - pyserial (Debian/Ubuntu package: python-serial) - - - :mod:`mopidy.mixers.osa` (OS X only) - - - No additional dependencies. +- Mixer dependencies: The default mixer does not require any additional + dependencies. If you use another mixer, see the mixer's docs for any + additional requirements. - Dependencies for at least one Mopidy backend: - - *Default:* :mod:`mopidy.backends.libspotify` (Linux, OS X, and Windows) + - The default backend, :mod:`mopidy.backends.libspotify`, requires libspotify + and pyspotify. See :doc:`libspotify`. - - :doc:`libspotify and pyspotify ` - - - :mod:`mopidy.backends.local` (Linux, OS X, and Windows) - - - No additional dependencies. + - The local backend, :mod:`mopidy.backends.local`, requires no additional + dependencies. - Optional dependencies: - - :mod:`mopidy.frontends.lastfm` - - - pylast >= 4.3.0 + - To use the Last.FM scrobbler, see :mod:`mopidy.frontends.lastfm` for + additional requirements. Install latest stable release diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py index f90060ce..4aa5952f 100644 --- a/mopidy/mixers/alsa.py +++ b/mopidy/mixers/alsa.py @@ -11,6 +11,10 @@ class AlsaMixer(BaseMixer): Mixer which uses the Advanced Linux Sound Architecture (ALSA) to control volume. + **Dependencies:** + + - pyalsaaudio >= 0.2 (python-alsaaudio on Debian/Ubuntu) + **Settings:** - :attr:`mopidy.settings.MIXER_ALSA_CONTROL` diff --git a/mopidy/mixers/osa.py b/mopidy/mixers/osa.py index 8d69eb47..2ea04cf2 100644 --- a/mopidy/mixers/osa.py +++ b/mopidy/mixers/osa.py @@ -4,7 +4,18 @@ import time from mopidy.mixers.base import BaseMixer class OsaMixer(BaseMixer): - """Mixer which uses ``osascript`` on OS X to control volume.""" + """ + Mixer which uses ``osascript`` on OS X to control volume. + + **Dependencies:** + + - None + + **Settings:** + + - None + + """ CACHE_TTL = 30 From ab6dbbb8fe41df9bc8e45feafe028ab7b65cea10 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 28 Dec 2010 15:19:15 +0100 Subject: [PATCH 179/235] Add note on the importancy of using pyspotify from the correct repo --- docs/installation/libspotify.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index 692bda36..b511e9c9 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -7,9 +7,9 @@ Mopidy uses `libspotify the Spotify music service. To use :mod:`mopidy.backends.libspotify` you must install libspotify and `pyspotify `_. -.. warning:: +.. note:: - This backend requires a `Spotify premium account + This backend requires a paid `Spotify premium account `_. .. note:: @@ -78,3 +78,8 @@ Check out the pyspotify code, and install it:: tar zxfv pyspotify.tar.gz cd pyspotify/pyspotify/ sudo python setup.py install + +It is important that you install pyspotify from the ``mopidy`` branch of the +``mopidy/pyspotify`` repository, as the upstream repository at +``winjer/pyspotify`` is not updated with changes needed to support e.g. +libspotify 0.0.6 and high bitrate audio. From 7743fa372b91e111abd2019e1260ce33cc753228 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 28 Dec 2010 15:31:10 +0100 Subject: [PATCH 180/235] Doc gst-launch test step. Fixes GH#35. --- docs/installation/gstreamer.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index ef66c673..ad2761a3 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -46,3 +46,19 @@ you should see a long listing of installed plugins, ending in a summary line:: $ gst-inspect-0.10 ... long list of installed plugins ... Total count: 218 plugins (1 blacklist entry not shown), 1031 features + +You should be able to produce a audible tone by running:: + + gst-launch-0.10 audiotestsrc ! autoaudiosink + +If you cannot hear any sound when running this command, you won't hear any +sound from Mopidy either, as Mopidy uses GStreamer's ``autoaudiosink`` to play +audio. Thus, make this work before you continue installing Mopidy. + + +Using a custom audio sink +========================= + +If you for some reason want to use some other GStreamer audio sink than +``autoaudiosink``, you can change :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK` +in your ``settings.py`` file. From 27f6e95e1f268439e8e6d700d1980810f32a4a42 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 28 Dec 2010 15:42:52 +0100 Subject: [PATCH 181/235] Minimize diff with master branch --- docs/installation/libspotify.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index b511e9c9..afb54d16 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -72,7 +72,7 @@ Install pyspotify's dependencies. At Debian/Ubuntu systems:: In OS X no additional dependencies are needed. -Check out the pyspotify code, and install it:: +Get the pyspotify code, and install it:: wget --no-check-certificate -O pyspotify.tar.gz https://github.com/mopidy/pyspotify/tarball/mopidy tar zxfv pyspotify.tar.gz From 19683539ea30e2c20900299ff609c028c3666df9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 31 Dec 2010 17:25:25 +0100 Subject: [PATCH 182/235] Make mopidy.desktop pass desktop-file-validate validation --- data/mopidy.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/mopidy.desktop b/data/mopidy.desktop index f5ca43bb..70257d58 100644 --- a/data/mopidy.desktop +++ b/data/mopidy.desktop @@ -7,4 +7,4 @@ Icon=audio-x-generic TryExec=mopidy Exec=mopidy Terminal=true -Categories=AudioVideo;Audio;Player;ConsoleOnly +Categories=AudioVideo;Audio;Player;ConsoleOnly; From e8aaefa5cb8e9b4c50df2d475edd04919555ac70 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 7 Jan 2011 19:04:41 +0100 Subject: [PATCH 183/235] Debian packaging has moved to its own repo --- debian/TODO | 14 -------------- debian/changelog | 5 ----- debian/compat | 1 - debian/control | 22 ---------------------- debian/copyright | 38 -------------------------------------- debian/docs | 2 -- debian/menu | 2 -- debian/pyversions | 1 - debian/rules | 27 --------------------------- debian/source/format | 1 - debian/watch | 2 -- 11 files changed, 115 deletions(-) delete mode 100644 debian/TODO delete mode 100644 debian/changelog delete mode 100644 debian/compat delete mode 100644 debian/control delete mode 100644 debian/copyright delete mode 100644 debian/docs delete mode 100644 debian/menu delete mode 100644 debian/pyversions delete mode 100755 debian/rules delete mode 100644 debian/source/format delete mode 100644 debian/watch diff --git a/debian/TODO b/debian/TODO deleted file mode 100644 index 4551dc55..00000000 --- a/debian/TODO +++ /dev/null @@ -1,14 +0,0 @@ -To do for Mopidy's Debian packaging -=================================== - -- Install data/mopidy.desktop into /usr/share/applications/ -- Add manpages for all commands. Build the manpages with Sphinx -- Make init script run Mopidy as a daemon -- Make init script run Mopidy with its own user -- Add support for reading settings from /etc/mopidy/settings.py -- Log to /var/log -- Cache files in /var/cache -- Package pyspotify and add it to Recommends -- Package pylast and add it to Recommends -- Create GPG key for signing the package -- Host the packages at PPA or apt.mopidy.com diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index a04f2e78..00000000 --- a/debian/changelog +++ /dev/null @@ -1,5 +0,0 @@ -mopidy (0.2.0-1) unstable; urgency=low - - * Initial release - - -- Stein Magnus Jodal Sun, 31 Oct 2010 13:07:04 +0100 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index 7f8f011e..00000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -7 diff --git a/debian/control b/debian/control deleted file mode 100644 index c2755717..00000000 --- a/debian/control +++ /dev/null @@ -1,22 +0,0 @@ -Source: mopidy -Section: sound -Priority: optional -Maintainer: Stein Magnus Jodal -Build-Depends: debhelper (>= 7.0.50~), python-support, python (>= 2.6), - python-sphinx (>= 1.0), python-pygraphviz -Standards-Version: 3.9.1 -Homepage: http://www.mopidy.com/ -Vcs-Git: git://github.com/jodal/mopidy.git -Vcs-Browser: http://github.com/jodal/mopidy - -Package: mopidy -Architecture: all -Depends: ${misc:Depends}, ${python:Depends}, python-gst0.10 -Recommends: gstreamer0.10-plugins-good, gstreamer0.10-plugins-ugly -Suggests: python-alsaaudio (>= 0.2), python-serial -Description: music server with MPD client support - Mopidy is a music server which can play music from Spotify or from your - local hard drive. To search for music in Spotify’s vast archive, manage - playlists, and play music, you can use most MPD clients. MPD clients are - available for most platforms, including Windows, Mac OS X, Linux, and - iPhone and Android phones. diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index c29416d6..00000000 --- a/debian/copyright +++ /dev/null @@ -1,38 +0,0 @@ -This work was packaged for Debian by: - - Stein Magnus Jodal on Sun, 31 Oct 2010 09:50:28 +0100 - -It was downloaded from: - - http://pypi.python.org/packages/source/M/Mopidy/Mopidy-0.2.0.tar.gz - -Upstream Author(s): - - Stein Magnus Jodal - -Copyright: - - Copyright 2009-2010 Stein Magnus Jodal and contributors - -License: - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied. See the License for the specific language governing - permissions and limitations under the License. - -On Debian systems, the complete text of the Apache version 2.0 license -can be found in "/usr/share/common-licenses/Apache-2.0". - -The Debian packaging is: - - Copyright 2010 Stein Magnus Jodal - -and is licensed under the Apache License, Version 2.0, see above. diff --git a/debian/docs b/debian/docs deleted file mode 100644 index a4b46448..00000000 --- a/debian/docs +++ /dev/null @@ -1,2 +0,0 @@ -README.rst -docs/_build/html/ diff --git a/debian/menu b/debian/menu deleted file mode 100644 index 6376a81e..00000000 --- a/debian/menu +++ /dev/null @@ -1,2 +0,0 @@ -?package(mopidy):needs="text" section="Applications/Sound"\ - title="Mopidy" command="/usr/bin/mopidy" diff --git a/debian/pyversions b/debian/pyversions deleted file mode 100644 index 0c043f18..00000000 --- a/debian/pyversions +++ /dev/null @@ -1 +0,0 @@ -2.6- diff --git a/debian/rules b/debian/rules deleted file mode 100755 index 926a81b1..00000000 --- a/debian/rules +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/make -f -# -*- makefile -*- -# Sample debian/rules that uses debhelper. -# This file was originally written by Joey Hess and Craig Small. -# As a special exception, when this file is copied by dh-make into a -# dh-make output file, you may use that output file without restriction. -# This special exception was added by Craig Small in version 0.37 of dh-make. - -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 - -%: - dh $@ - -override_dh_clean: - make -C docs/ clean - dh_clean - -override_dh_installchangelogs: - dh_installchangelogs docs/changes.rst - -override_dh_installdocs: - make -C docs/ clean html - dh_installdocs - -.PHONY: override_dh_clean override_dh_installchangelogs \ - override_dh_installdocs override_dh_installinit diff --git a/debian/source/format b/debian/source/format deleted file mode 100644 index 163aaf8d..00000000 --- a/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (quilt) diff --git a/debian/watch b/debian/watch deleted file mode 100644 index 3d4d3a41..00000000 --- a/debian/watch +++ /dev/null @@ -1,2 +0,0 @@ -version=3 -http://pypi.python.org/packages/source/M/Mopidy/Mopidy-(.*)\.tar\.gz From 2161e6cb94840e5d1345ab6ad63847aa5963e7ee Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 7 Jan 2011 22:52:11 +0100 Subject: [PATCH 184/235] docs: Remove empty section on installing libspotify on Windows --- docs/installation/libspotify.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index afb54d16..cb352b8c 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -55,12 +55,6 @@ When libspotify has been installed, continue with :ref:`pyspotify_installation`. -Install libspotify on Windows -============================= - -**TODO** Test and document installation on Windows. - - .. _pyspotify_installation: Installing pyspotify From 0212e7181b8de646d75e7d532e6178508d114e74 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Jan 2011 01:10:49 +0100 Subject: [PATCH 185/235] docs: Darker background on pre tags --- docs/_themes/nature/static/nature.css_t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_themes/nature/static/nature.css_t b/docs/_themes/nature/static/nature.css_t index 63ef80d6..b6c0f22e 100644 --- a/docs/_themes/nature/static/nature.css_t +++ b/docs/_themes/nature/static/nature.css_t @@ -214,7 +214,7 @@ p.admonition-title:after { pre { padding: 10px; - background-color: #fafafa; + background-color: #eeeeee; color: #222222; line-height: 1.5em; font-size: 1.1em; From 7e0047a8b5d77f877a3d0bc5a16169d94a6bd357 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Jan 2011 01:25:13 +0100 Subject: [PATCH 186/235] docs: Rewrite installation docs and include the APT archive alternative --- docs/installation/gstreamer.rst | 29 +++++-- docs/installation/index.rst | 136 ++++++++++++++++++++++++------- docs/installation/libspotify.rst | 53 ++++++++++-- 3 files changed, 173 insertions(+), 45 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index ad2761a3..72d55908 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -5,23 +5,32 @@ GStreamer installation To use the Mopidy, you first need to install GStreamer and its Python bindings. -Installing GStreamer on Linux -============================= +Installing GStreamer +==================== -GStreamer is packaged for most popular Linux distributions. If you use -Debian/Ubuntu you can install GStreamer with Aptitude:: +On Linux +-------- - sudo aptitude install python-gst0.10 gstreamer0.10-plugins-good \ +GStreamer is packaged for most popular Linux distributions. Search for +GStreamer in your package manager, and make sure to install the Python +bindings, and the "good" and "ugly" plugin sets. + +If you use Debian/Ubuntu you can install GStreamer like this:: + + sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \ gstreamer0.10-plugins-ugly +If you install Mopidy from our APT archive, you don't need to install GStreamer +yourself. The Mopidy Debian package will handle it for you. -Installing GStreamer on OS X -============================ + +On OS X from Homebrew +--------------------- .. note:: We have created GStreamer formulas for Homebrew to make the GStreamer - installation easy for you, but our formulas has not been merged into + installation easy for you, but not all our formulas have been merged into Homebrew's master branch yet. You should either fetch the formula files from `Homebrew's issue #1612 `_ yourself, or fall @@ -31,6 +40,10 @@ To install GStreamer on OS X using Homebrew:: brew install gst-python gst-plugins-good gst-plugins-ugly + +On OS X from MacPorts +--------------------- + To install GStreamer on OS X using MacPorts:: sudo port install py26-gst-python gstreamer-plugins-good \ diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 142c5d84..d45ac1c9 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -2,10 +2,9 @@ Installation ************ -To get a basic version of Mopidy running, you need Python and the -:doc:`GStreamer library `. To use Spotify with Mopidy, you also need -:doc:`libspotify and pyspotify `. Mopidy itself can either be -installed from the Python package index, PyPI, or from git. +There are several ways to install Mopidy. What way is best depends upon your +setup and whether you want to use stable releases or less stable development +versions. Install dependencies @@ -17,7 +16,10 @@ Install dependencies gstreamer libspotify -Make sure you got the required dependencies installed. +If you install Mopidy from the APT archive, as described below, you can skip +the dependency installation part. + +Otherwise, make sure you got the required dependencies installed. - Python >= 2.6, < 3 @@ -44,49 +46,125 @@ Make sure you got the required dependencies installed. Install latest stable release ============================= -To install the currently latest stable release of Mopidy using ``pip``:: - sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian - sudo brew install pip # On OS X - sudo pip install -U Mopidy +From APT archive +---------------- -To later upgrade to the latest release, just rerun the last command. +If you run a Debian based Linux distribution, like Ubuntu, the easiest way to +install Mopidy is from the Mopidy APT archive. When installing from the APT +archive, you will automatically get updates to Mopidy in the same way as you +get updates to the rest of your distribution. -If you for some reason can't use ``pip``, try ``easy_install``. +#. Add the archive's GPG key:: -Next, you need to set a couple of :doc:`settings `, and then you're -ready to :doc:`run Mopidy `. + wget -q -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add - + +#. Add the following to ``/etc/apt/sources.list``, or if you have the directory + ``/etc/apt/sources.list.d/``, add it to a file called ``mopidy.list`` in + that directory:: + + # Mopidy APT archive + deb http://apt.mopidy.com/ stable main contrib non-free + deb-src http://apt.mopidy.com/ stable main contrib non-free + +#. Install Mopidy and all dependencies:: + + sudo apt-get update + sudo apt-get install mopidy + +#. Next, you need to set a couple of :doc:`settings `, and then + you're ready to :doc:`run Mopidy `. + +When a new release is out, and you can't wait for you system to figure it out +for itself, run the following to force an upgrade:: + + sudo apt-get update + sudo apt-get dist-upgrade -Install development snapshot -============================ +From PyPI using Pip +------------------- -If you want to follow Mopidy development closer, you may install a snapshot of -Mopidy's ``develop`` branch:: +If you are on OS X or on Linux, but can't install from the APT archive, you can +install Mopidy from PyPI using Pip. - sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian - sudo brew install pip # On OS X - sudo pip install mopidy==dev +#. When you install using Pip, you first need to ensure that all of Mopidy's + dependencies have been installed. See the section on dependencies above. -Next, you need to set a couple of :doc:`settings `, and then you're -ready to :doc:`run Mopidy `. +#. Then, you need to install Pip:: + + sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian + sudo brew install pip # On OS X + +#. To install the currently latest stable release of Mopidy:: + + sudo pip install -U Mopidy + + To upgrade Mopidy to future releases, just rerun this command. + +#. Next, you need to set a couple of :doc:`settings `, and then + you're ready to :doc:`run Mopidy `. + +If you for some reason can't use Pip, try ``easy_install`` instead. -Track development using Git +Install development version =========================== -If you want to contribute to Mopidy, you should install Mopidy using Git:: +If you want to follow the development of Mopidy closer, you may install a +development version of Mopidy. These are not as stable as the releases, but +you'll get access to new features earlier and may help us by reporting issues. - sudo aptitude install git-core # On Ubuntu/Debian - sudo brew install git # On OS X - git clone git://github.com/mopidy/mopidy.git -You can then run Mopidy directly from the Git repository:: +From snapshot +------------- + +If you want to follow Mopidy development closer, you may install a snapshot of +Mopidy's ``develop`` branch. + +#. When you install using Pip, you first need to ensure that all of Mopidy's + dependencies have been installed. See the section on dependencies above. + +#. Then, you need to install Pip:: + + sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian + sudo brew install pip # On OS X + +#. To install the latest snapshot of Mopidy, run:: + + sudo pip install mopidy==dev + + To upgrade Mopidy to future releases, just rerun this command. + +#. Next, you need to set a couple of :doc:`settings `, and then + you're ready to :doc:`run Mopidy `. + + +From Git +-------- + +If you want to contribute to Mopidy, you should install Mopidy using Git. + +#. When you install from Git, you first need to ensure that all of Mopidy's + dependencies have been installed. See the section on dependencies above. + +#. Then install Git, if haven't already:: + + sudo aptitude install git-core # On Ubuntu/Debian + sudo brew install git # On OS X + +#. Clone the official Mopidy repository, or your own fork of it:: + + git clone git://github.com/mopidy/mopidy.git + +#. Next, you need to set a couple of :doc:`settings `. + +#. You can then run Mopidy directly from the Git repository:: cd mopidy/ # Move into the Git repo dir python mopidy # Run python on the mopidy source code dir -To get the latest changes to Mopidy:: +#. Later, to get the latest changes to Mopidy:: cd mopidy/ git pull diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index cb352b8c..5d278fe2 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -19,8 +19,25 @@ install libspotify and `pyspotify `_. Spotify Group. -Installing libspotify on Linux -============================== +Installing libspotify +===================== + + +On Linux from APT archive +------------------------- + +If you run a Debian based Linux distribution, like Ubuntu, see +http://apt.mopidy.com/ for how to the Mopidy APT archive as a software source +on your installation. Then, simply run:: + + sudo apt-get install libspotify6 + +When libspotify has been installed, continue with +:ref:`pyspotify_installation`. + + +On Linux from source +-------------------- Download and install libspotify 0.0.6 for your OS and CPU architecture from https://developer.spotify.com/en/libspotify/. @@ -37,8 +54,8 @@ When libspotify has been installed, continue with :ref:`pyspotify_installation`. -Installing libspotify on OS X -============================= +On OS X from Homebrew +--------------------- In OS X you need to have `XCode `_ and `Homebrew `_ installed. Then, to install @@ -60,17 +77,37 @@ When libspotify has been installed, continue with Installing pyspotify ==================== -Install pyspotify's dependencies. At Debian/Ubuntu systems:: +When you've installed libspotify, it's time for making it available from Python +by installing pyspotify. - sudo aptitude install python-dev -In OS X no additional dependencies are needed. +On Linux from APT archive +------------------------- + +Assuming that you've already set up http://apt.mopidy.com/ as a software +source, run:: + + sudo apt-get install python-spotify + +If you haven't already installed libspotify, this command will install both +libspotify and pyspotify for you. + + +On Linux/OS X from source +------------------------- + +On Linux, you need to get the Python development files installed. On +Debian/Ubuntu systems run:: + + sudo apt-get install python-dev + +On OS X no additional dependencies are needed. Get the pyspotify code, and install it:: wget --no-check-certificate -O pyspotify.tar.gz https://github.com/mopidy/pyspotify/tarball/mopidy tar zxfv pyspotify.tar.gz - cd pyspotify/pyspotify/ + cd pyspotify/ sudo python setup.py install It is important that you install pyspotify from the ``mopidy`` branch of the From bb57ad9f60abfaf2e31b187c7644ac5341f842c7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Jan 2011 01:33:45 +0100 Subject: [PATCH 187/235] docs: Add note ArchLinux AUR package --- docs/installation/index.rst | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index d45ac1c9..df41f521 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -116,8 +116,8 @@ development version of Mopidy. These are not as stable as the releases, but you'll get access to new features earlier and may help us by reporting issues. -From snapshot -------------- +From snapshot using Pip +----------------------- If you want to follow Mopidy development closer, you may install a snapshot of Mopidy's ``develop`` branch. @@ -172,3 +172,10 @@ If you want to contribute to Mopidy, you should install Mopidy using Git. For an introduction to ``git``, please visit `git-scm.com `_. Also, please read our :doc:`developer documentation `. + + +From AUR on ArchLinux +--------------------- + +If you are running ArchLinux, you can install a development snapshot of Mopidy +using the package found at http://aur.archlinux.org/packages.php?ID=44026. From 35e35054f0aa041d5a570fada0a51a91010a00e2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Jan 2011 01:42:33 +0100 Subject: [PATCH 188/235] docs: Update development roadmap --- docs/development/roadmap.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst index 175c62ac..9db74a4d 100644 --- a/docs/development/roadmap.rst +++ b/docs/development/roadmap.rst @@ -27,7 +27,7 @@ Possible targets for the next version - Write-support for Spotify, i.e. playlist management. - Virtual directories with e.g. starred tracks from Spotify. - - **[WIP: possibly v0.3]** Support for 320 kbps audio. + - **[DONE: v0.3]** Support for 320 kbps audio. - Local backend: @@ -47,7 +47,7 @@ Stuff we want to do, but not right now, and maybe never recipies for all our dependencies and Mopidy itself to make OS X installation a breeze. See `Homebrew's issue #1612 `_. - - **[WIP]** Create `Debian packages + - **[DONE]** Create `Debian packages `_ of all our dependencies and Mopidy itself (hosted in our own Debian repo until we get stuff into the various distros) to make Debian/Ubuntu installation a breeze. From 3989891de5d17fe91b7a7d4bcffa8350826e5e16 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Jan 2011 02:47:16 +0100 Subject: [PATCH 189/235] docs: Better instructions for ArchLinux (thanks to sandsmark) --- docs/installation/index.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index df41f521..746648f4 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -179,3 +179,12 @@ From AUR on ArchLinux If you are running ArchLinux, you can install a development snapshot of Mopidy using the package found at http://aur.archlinux.org/packages.php?ID=44026. + +To install it, you can use ``packer``, ``yaourt``, or do it by hand like this:: + + wget http://aur.archlinux.org/packages/mopidy-git/mopidy-git.tar.gz + tar xf mopidy-git.tar.gz + cd mopidy-git/ + makepkg -si + +To upgrade Mopidy to future releases, just rerun ``makepkg``. From 6a29222751468ccc81dd8bdec6349382be83da7b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Jan 2011 02:58:50 +0100 Subject: [PATCH 190/235] docs: Split ArchLinux instructions into several steps, like the others --- docs/installation/index.rst | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 746648f4..f6754371 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -180,11 +180,18 @@ From AUR on ArchLinux If you are running ArchLinux, you can install a development snapshot of Mopidy using the package found at http://aur.archlinux.org/packages.php?ID=44026. -To install it, you can use ``packer``, ``yaourt``, or do it by hand like this:: +#. First, you should consider installing any optional dependencies not included + by the AUR package, like required for e.g. Last.fm scrobbling. - wget http://aur.archlinux.org/packages/mopidy-git/mopidy-git.tar.gz - tar xf mopidy-git.tar.gz - cd mopidy-git/ - makepkg -si +#. To install Mopidy with GStreamer, libspotify and pyspotify, you can use + ``packer``, ``yaourt``, or do it by hand like this:: -To upgrade Mopidy to future releases, just rerun ``makepkg``. + wget http://aur.archlinux.org/packages/mopidy-git/mopidy-git.tar.gz + tar xf mopidy-git.tar.gz + cd mopidy-git/ + makepkg -si + + To upgrade Mopidy to future releases, just rerun ``makepkg``. + +#. Next, you need to set a couple of :doc:`settings `, and then + you're ready to :doc:`run Mopidy `. From dbbde5fdd6f685a7cfdf17c2f7c7328f296795e1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Jan 2011 03:48:16 +0100 Subject: [PATCH 191/235] Update Last.fm frontend to use pylast>=0.5 and the new Scrobbling 2.0 API --- docs/changes.rst | 7 +++++++ mopidy/frontends/lastfm.py | 32 +++++++++++++++----------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 4a8548bf..624f7ef4 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -21,6 +21,13 @@ No description yet. - Support high bitrate (320k) audio. See :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE` for details. +- Last.fm frontend: + + - If you use the Last.fm frontend, you need to upgrade to pylast 0.5. + + - Update to use Last.fm's new Scrobbling 2.0 API, as the old Submissions + Protocol 1.2.1 is deprecated. (Fixes: :issue:`33`) + **Changes** diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index ddd621f8..8d912f64 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -15,8 +15,8 @@ from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.frontends.lastfm') -CLIENT_ID = u'mop' -CLIENT_VERSION = get_version() +API_KEY = '2236babefa8ebb3d93ea467560d00d04' +API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd' # pylast raises UnicodeEncodeError on conversion from unicode objects to # ascii-encoded bytestrings, so we explicitly encode as utf-8 before passing @@ -34,7 +34,7 @@ class LastfmFrontend(BaseFrontend): **Dependencies:** - - `pylast `_ >= 0.4.30 + - `pylast `_ >= 0.5 **Settings:** @@ -64,12 +64,11 @@ class LastfmFrontendThread(BaseThread): self.name = u'LastfmFrontendThread' self.connection = connection self.lastfm = None - self.scrobbler = None self.last_start_time = None def run_inside_try(self): self.setup() - while self.scrobbler is not None: + while self.lastfm is not None: self.connection.poll(None) message = self.connection.recv() self.process_message(message) @@ -78,10 +77,9 @@ class LastfmFrontendThread(BaseThread): try: username = settings.LASTFM_USERNAME password_hash = pylast.md5(settings.LASTFM_PASSWORD) - self.lastfm = pylast.get_lastfm_network( + self.lastfm = pylast.LastFMNetwork( + api_key=API_KEY, api_secret=API_SECRET, username=username, password_hash=password_hash) - self.scrobbler = self.lastfm.get_scrobbler( - CLIENT_ID, CLIENT_VERSION) logger.info(u'Connected to Last.fm') except SettingsError as e: logger.info(u'Last.fm scrobbler not started') @@ -103,12 +101,13 @@ class LastfmFrontendThread(BaseThread): self.last_start_time = int(time.time()) logger.debug(u'Now playing track: %s - %s', artists, track.name) try: - self.scrobbler.report_now_playing( + self.lastfm.update_now_playing( artists.encode(ENCODING), track.name.encode(ENCODING), album=track.album.name.encode(ENCODING), - duration=duration, - track_number=track.track_no) + duration=str(duration), + track_number=str(track.track_no), + mbid=(track.musicbrainz_id or '').encode(ENCODING)) except (pylast.ScrobblingError, socket.error) as e: logger.warning(u'Last.fm now playing error: %s', e) @@ -127,14 +126,13 @@ class LastfmFrontendThread(BaseThread): self.last_start_time = int(time.time()) - duration logger.debug(u'Scrobbling track: %s - %s', artists, track.name) try: - self.scrobbler.scrobble( + self.lastfm.scrobble( artists.encode(ENCODING), track.name.encode(ENCODING), - time_started=self.last_start_time, - source=pylast.SCROBBLE_SOURCE_USER, - mode=pylast.SCROBBLE_MODE_PLAYED, - duration=duration, + str(self.last_start_time), album=track.album.name.encode(ENCODING), - track_number=track.track_no) + track_number=str(track.track_no), + duration=str(duration), + mbid=(track.musicbrainz_id or '').encode(ENCODING)) except (pylast.ScrobblingError, socket.error) as e: logger.warning(u'Last.fm scrobbling error: %s', e) From 85d4dde338c6b8b02bf52952fa24ed781e7c3079 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 9 Jan 2011 13:04:28 +0100 Subject: [PATCH 192/235] Forgot to update pylast version in requirements file --- requirements/lastfm.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/lastfm.txt b/requirements/lastfm.txt index 642735be..887a0f0d 100644 --- a/requirements/lastfm.txt +++ b/requirements/lastfm.txt @@ -1 +1 @@ -pylast >= 0.4.30 +pylast >= 0.5 From a14f114d2aed2f45a41ca8809da095f684b6ca2d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 9 Jan 2011 22:54:18 +0100 Subject: [PATCH 193/235] Fix crash for Spotify users with playlist folders --- docs/changes.rst | 4 ++++ mopidy/backends/libspotify/session_manager.py | 1 + mopidy/backends/libspotify/translator.py | 22 ++++++++++++++----- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 624f7ef4..72e9217b 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -21,6 +21,10 @@ No description yet. - Support high bitrate (320k) audio. See :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE` for details. + - Catch and log error caused by playlist folder boundaries being threated as + normal playlists. More permanent fix requires support for checking playlist + types in pyspotify. + - Last.fm frontend: - If you use the Last.fm frontend, you need to upgrade to pylast 0.5. diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/libspotify/session_manager.py index 8a79088f..f736a40e 100644 --- a/mopidy/backends/libspotify/session_manager.py +++ b/mopidy/backends/libspotify/session_manager.py @@ -54,6 +54,7 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread): for spotify_playlist in session.playlist_container(): playlists.append( LibspotifyTranslator.to_mopidy_playlist(spotify_playlist)) + playlists = filter(None, playlists) self.core_queue.put({ 'command': 'set_stored_playlists', 'playlists': playlists, diff --git a/mopidy/backends/libspotify/translator.py b/mopidy/backends/libspotify/translator.py index 09303eda..4a42cf97 100644 --- a/mopidy/backends/libspotify/translator.py +++ b/mopidy/backends/libspotify/translator.py @@ -1,11 +1,14 @@ import datetime as dt +import logging -from spotify import Link +from spotify import Link, SpotifyError from mopidy import settings from mopidy.backends.libspotify import ENCODING from mopidy.models import Artist, Album, Track, Playlist +logger = logging.getLogger('mopidy.backends.libspotify.translator') + class LibspotifyTranslator(object): @classmethod def to_mopidy_artist(cls, spotify_artist): @@ -47,8 +50,15 @@ class LibspotifyTranslator(object): def to_mopidy_playlist(cls, spotify_playlist): if not spotify_playlist.is_loaded(): return Playlist(name=u'[loading...]') - return Playlist( - uri=str(Link.from_playlist(spotify_playlist)), - name=spotify_playlist.name().decode(ENCODING), - tracks=[cls.to_mopidy_track(t) for t in spotify_playlist], - ) + # FIXME Replace this try-except with a check on the playlist type, + # which is currently not supported by pyspotify, to avoid handling + # playlist folder boundaries like normal playlists. + try: + return Playlist( + uri=str(Link.from_playlist(spotify_playlist)), + name=spotify_playlist.name().decode(ENCODING), + tracks=[cls.to_mopidy_track(t) for t in spotify_playlist], + ) + except SpotifyError, e: + logger.warning(u'Failed translating Spotify playlist ' + '(probably a playlist folder boundary): %s', e) From 469e5fa189774b87e711b6480375175576c85eeb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 9 Jan 2011 23:38:46 +0100 Subject: [PATCH 194/235] Fix volume setting for Droid MPD --- docs/changes.rst | 5 +++++ mopidy/frontends/mpd/protocol/playback.py | 5 +++++ tests/frontends/mpd/playback_test.py | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 72e9217b..931e99cf 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -54,6 +54,11 @@ No description yet. application menus. - Create infrastructure for creating Debian packages of Mopidy. +- MPD frontend: + + - Support ``setvol 50`` without quotes around the argument. Fixes volume + control in Droid MPD. + - Local backend: - Add :command:`mopidy-scan` command to generate ``tag_cache`` files without diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 2f5dd29e..13f7d2a8 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -320,6 +320,7 @@ def seekid(frontend, cpid, seconds): playid(frontend, cpid) frontend.backend.playback.seek(int(seconds) * 1000) +@handle_pattern(r'^setvol (?P[-+]*\d+)$') @handle_pattern(r'^setvol "(?P[-+]*\d+)"$') def setvol(frontend, volume): """ @@ -328,6 +329,10 @@ def setvol(frontend, volume): ``setvol {VOL}`` Sets volume to ``VOL``, the range of volume is 0-100. + + *Droid MPD:* + + - issues ``setvol 50`` without quotes around the argument. """ volume = int(volume) if volume < 0: diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index 4e60546d..f6e22641 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -104,6 +104,11 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) self.assertEqual(10, self.b.mixer.volume) + def test_setvol_without_quotes(self): + result = self.h.handle_request(u'setvol 50') + self.assert_(u'OK' in result) + self.assertEqual(50, self.b.mixer.volume) + def test_single_off(self): result = self.h.handle_request(u'single "0"') self.assertFalse(self.b.playback.single) From 3eb1d477651ef92c13da21995279c4ad1ae856ad Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 9 Jan 2011 23:46:03 +0100 Subject: [PATCH 195/235] Fix seek for Droid MPD --- docs/changes.rst | 2 ++ mopidy/frontends/mpd/protocol/playback.py | 5 +++++ tests/frontends/mpd/playback_test.py | 7 +++++++ 3 files changed, 14 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 931e99cf..03044731 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -58,6 +58,8 @@ No description yet. - Support ``setvol 50`` without quotes around the argument. Fixes volume control in Droid MPD. + - Support ``seek 1 120`` without quotes around the arguments. Fixes seek in + Droid MPD. - Local backend: diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 13f7d2a8..19922bc3 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -293,6 +293,7 @@ def replay_gain_status(frontend): """ return u'off' # TODO +@handle_pattern(r'^seek (?P\d+) (?P\d+)$') @handle_pattern(r'^seek "(?P\d+)" "(?P\d+)"$') def seek(frontend, songpos, seconds): """ @@ -302,6 +303,10 @@ def seek(frontend, songpos, seconds): Seeks to the position ``TIME`` (in seconds) of entry ``SONGPOS`` in the playlist. + + *Droid MPD:* + + - issues ``seek 1 120`` without quotes around the arguments. """ if frontend.backend.playback.current_playlist_position != songpos: playpos(frontend, songpos) diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index f6e22641..43614173 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -325,6 +325,13 @@ class PlaybackControlHandlerTest(unittest.TestCase): result = self.h.handle_request(u'seek "1" "30"') self.assertEqual(self.b.playback.current_track, seek_track) + def test_seek_without_quotes(self): + self.b.current_playlist.append([Track(length=40000)]) + self.h.handle_request(u'seek 0') + result = self.h.handle_request(u'seek 0 30') + self.assert_(u'OK' in result) + self.assert_(self.b.playback.time_position >= 30000) + def test_seekid(self): self.b.current_playlist.append([Track(length=40000)]) result = self.h.handle_request(u'seekid "0" "30"') From 8503fe7b9e32210eedf39cbc674c447be1f795ab Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 10 Jan 2011 16:08:05 +0100 Subject: [PATCH 196/235] pylast 0.5 seems to prefer unicode over utf-8 encoded bytestrings --- mopidy/frontends/lastfm.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index 8d912f64..60c2d708 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -18,11 +18,6 @@ logger = logging.getLogger('mopidy.frontends.lastfm') API_KEY = '2236babefa8ebb3d93ea467560d00d04' API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd' -# pylast raises UnicodeEncodeError on conversion from unicode objects to -# ascii-encoded bytestrings, so we explicitly encode as utf-8 before passing -# strings to pylast. -ENCODING = u'utf-8' - class LastfmFrontend(BaseFrontend): """ Frontend which scrobbles the music you play to your `Last.fm @@ -102,12 +97,12 @@ class LastfmFrontendThread(BaseThread): logger.debug(u'Now playing track: %s - %s', artists, track.name) try: self.lastfm.update_now_playing( - artists.encode(ENCODING), - track.name.encode(ENCODING), - album=track.album.name.encode(ENCODING), + artists, + track.name, + album=track.album.name, duration=str(duration), track_number=str(track.track_no), - mbid=(track.musicbrainz_id or '').encode(ENCODING)) + mbid=(track.musicbrainz_id or '')) except (pylast.ScrobblingError, socket.error) as e: logger.warning(u'Last.fm now playing error: %s', e) @@ -127,12 +122,12 @@ class LastfmFrontendThread(BaseThread): logger.debug(u'Scrobbling track: %s - %s', artists, track.name) try: self.lastfm.scrobble( - artists.encode(ENCODING), - track.name.encode(ENCODING), + artists, + track.name, str(self.last_start_time), - album=track.album.name.encode(ENCODING), + album=track.album.name, track_number=str(track.track_no), duration=str(duration), - mbid=(track.musicbrainz_id or '').encode(ENCODING)) + mbid=(track.musicbrainz_id or '')) except (pylast.ScrobblingError, socket.error) as e: logger.warning(u'Last.fm scrobbling error: %s', e) From 099544d9158c709fe051cb9b04ec6a7382d968b3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 10 Jan 2011 16:45:44 +0100 Subject: [PATCH 197/235] Rename libspotify backend to simply 'spotify', as suggested by adamcik and knutz3n half a year ago --- docs/api/backends/providers.rst | 2 +- docs/changes.rst | 4 ++ docs/installation/index.rst | 2 +- docs/modules/backends/libspotify.rst | 7 --- docs/modules/backends/spotify.rst | 7 +++ .../{libspotify => spotify}/__init__.py | 49 +++++++++--------- .../{libspotify => spotify}/library.py | 10 ++-- .../{libspotify => spotify}/playback.py | 4 +- .../session_manager.py | 19 +++---- .../spotify_appkey.key | Bin .../stored_playlists.py | 2 +- .../{libspotify => spotify}/translator.py | 6 +-- mopidy/settings.py | 16 +++--- 13 files changed, 67 insertions(+), 61 deletions(-) delete mode 100644 docs/modules/backends/libspotify.rst create mode 100644 docs/modules/backends/spotify.rst rename mopidy/backends/{libspotify => spotify}/__init__.py (61%) rename mopidy/backends/{libspotify => spotify}/library.py (87%) rename mopidy/backends/{libspotify => spotify}/playback.py (91%) rename mopidy/backends/{libspotify => spotify}/session_manager.py (87%) rename mopidy/backends/{libspotify => spotify}/spotify_appkey.key (100%) rename mopidy/backends/{libspotify => spotify}/stored_playlists.py (84%) rename mopidy/backends/{libspotify => spotify}/translator.py (93%) diff --git a/docs/api/backends/providers.rst b/docs/api/backends/providers.rst index 9289dd06..903e220b 100644 --- a/docs/api/backends/providers.rst +++ b/docs/api/backends/providers.rst @@ -37,5 +37,5 @@ Backend provider implementations ================================ * :mod:`mopidy.backends.dummy` -* :mod:`mopidy.backends.libspotify` +* :mod:`mopidy.backends.spotify` * :mod:`mopidy.backends.local` diff --git a/docs/changes.rst b/docs/changes.rst index 03044731..ddb46bb8 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -21,6 +21,10 @@ No description yet. - Support high bitrate (320k) audio. See :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE` for details. + - Rename :mod:`mopidy.backends.libspotify` to :mod:`mopidy.backends.spotify`. + If you have set :attr:`mopidy.settings.BACKENDS` explicitly, you may need + to update the setting's value. + - Catch and log error caused by playlist folder boundaries being threated as normal playlists. More permanent fix requires support for checking playlist types in pyspotify. diff --git a/docs/installation/index.rst b/docs/installation/index.rst index f6754371..26b50994 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -31,7 +31,7 @@ Otherwise, make sure you got the required dependencies installed. - Dependencies for at least one Mopidy backend: - - The default backend, :mod:`mopidy.backends.libspotify`, requires libspotify + - The default backend, :mod:`mopidy.backends.spotify`, requires libspotify and pyspotify. See :doc:`libspotify`. - The local backend, :mod:`mopidy.backends.local`, requires no additional diff --git a/docs/modules/backends/libspotify.rst b/docs/modules/backends/libspotify.rst deleted file mode 100644 index e7528757..00000000 --- a/docs/modules/backends/libspotify.rst +++ /dev/null @@ -1,7 +0,0 @@ -******************************************************* -:mod:`mopidy.backends.libspotify` -- Libspotify backend -******************************************************* - -.. automodule:: mopidy.backends.libspotify - :synopsis: Spotify backend using the libspotify library - :members: diff --git a/docs/modules/backends/spotify.rst b/docs/modules/backends/spotify.rst new file mode 100644 index 00000000..938d6337 --- /dev/null +++ b/docs/modules/backends/spotify.rst @@ -0,0 +1,7 @@ +************************************************* +:mod:`mopidy.backends.spotify` -- Spotify backend +************************************************* + +.. automodule:: mopidy.backends.spotify + :synopsis: Backend for the Spotify music streaming service + :members: diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/spotify/__init__.py similarity index 61% rename from mopidy/backends/libspotify/__init__.py rename to mopidy/backends/spotify/__init__.py index ad2926c7..d36f6250 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -4,54 +4,55 @@ from mopidy import settings from mopidy.backends.base import (Backend, CurrentPlaylistController, LibraryController, PlaybackController, StoredPlaylistsController) -logger = logging.getLogger('mopidy.backends.libspotify') +logger = logging.getLogger('mopidy.backends.spotify') ENCODING = 'utf-8' -class LibspotifyBackend(Backend): +class SpotifyBackend(Backend): """ - A `Spotify `_ backend which uses the official - `libspotify `_ - library and the `pyspotify `_ Python - bindings for libspotify. - - **Issues:** - http://github.com/mopidy/mopidy/issues/labels/backend-libspotify - - **Settings:** - - - :attr:`mopidy.settings.SPOTIFY_CACHE_PATH` - - :attr:`mopidy.settings.SPOTIFY_USERNAME` - - :attr:`mopidy.settings.SPOTIFY_PASSWORD` + A backend for playing music from the `Spotify `_ + music streaming service. The backend uses the official `libspotify + `_ library and the + `pyspotify `_ Python bindings for + libspotify. .. note:: This product uses SPOTIFY(R) CORE but is not endorsed, certified or otherwise approved in any way by Spotify. Spotify is the registered trade mark of the Spotify Group. + + **Issues:** + http://github.com/mopidy/mopidy/issues/labels/backend-spotify + + **Settings:** + + - :attr:`mopidy.settings.SPOTIFY_CACHE_PATH` + - :attr:`mopidy.settings.SPOTIFY_USERNAME` + - :attr:`mopidy.settings.SPOTIFY_PASSWORD` """ # Imports inside methods are to prevent loading of __init__.py to fail on # missing spotify dependencies. def __init__(self, *args, **kwargs): - from .library import LibspotifyLibraryProvider - from .playback import LibspotifyPlaybackProvider - from .stored_playlists import LibspotifyStoredPlaylistsProvider + from .library import SpotifyLibraryProvider + from .playback import SpotifyPlaybackProvider + from .stored_playlists import SpotifyStoredPlaylistsProvider - super(LibspotifyBackend, self).__init__(*args, **kwargs) + super(SpotifyBackend, self).__init__(*args, **kwargs) self.current_playlist = CurrentPlaylistController(backend=self) - library_provider = LibspotifyLibraryProvider(backend=self) + library_provider = SpotifyLibraryProvider(backend=self) self.library = LibraryController(backend=self, provider=library_provider) - playback_provider = LibspotifyPlaybackProvider(backend=self) + playback_provider = SpotifyPlaybackProvider(backend=self) self.playback = PlaybackController(backend=self, provider=playback_provider) - stored_playlists_provider = LibspotifyStoredPlaylistsProvider( + stored_playlists_provider = SpotifyStoredPlaylistsProvider( backend=self) self.stored_playlists = StoredPlaylistsController(backend=self, provider=stored_playlists_provider) @@ -61,11 +62,11 @@ class LibspotifyBackend(Backend): self.spotify = self._connect() def _connect(self): - from .session_manager import LibspotifySessionManager + from .session_manager import SpotifySessionManager logger.info(u'Mopidy uses SPOTIFY(R) CORE') logger.debug(u'Connecting to Spotify') - spotify = LibspotifySessionManager( + spotify = SpotifySessionManager( settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD, core_queue=self.core_queue, output=self.output) diff --git a/mopidy/backends/libspotify/library.py b/mopidy/backends/spotify/library.py similarity index 87% rename from mopidy/backends/libspotify/library.py rename to mopidy/backends/spotify/library.py index 948c69b2..16391473 100644 --- a/mopidy/backends/libspotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -4,13 +4,13 @@ import multiprocessing from spotify import Link, SpotifyError from mopidy.backends.base import BaseLibraryProvider -from mopidy.backends.libspotify import ENCODING -from mopidy.backends.libspotify.translator import LibspotifyTranslator +from mopidy.backends.spotify import ENCODING +from mopidy.backends.spotify.translator import SpotifyTranslator from mopidy.models import Playlist -logger = logging.getLogger('mopidy.backends.libspotify.library') +logger = logging.getLogger('mopidy.backends.spotify.library') -class LibspotifyLibraryProvider(BaseLibraryProvider): +class SpotifyLibraryProvider(BaseLibraryProvider): def find_exact(self, **query): return self.search(**query) @@ -20,7 +20,7 @@ class LibspotifyLibraryProvider(BaseLibraryProvider): # TODO Block until metadata_updated callback is called. Before that # the track will be unloaded, unless it's already in the stored # playlists. - return LibspotifyTranslator.to_mopidy_track(spotify_track) + return SpotifyTranslator.to_mopidy_track(spotify_track) except SpotifyError as e: logger.warning(u'Failed to lookup: %s', uri, e) return None diff --git a/mopidy/backends/libspotify/playback.py b/mopidy/backends/spotify/playback.py similarity index 91% rename from mopidy/backends/libspotify/playback.py rename to mopidy/backends/spotify/playback.py index 29409ff4..a066d90e 100644 --- a/mopidy/backends/libspotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -4,9 +4,9 @@ from spotify import Link, SpotifyError from mopidy.backends.base import BasePlaybackProvider -logger = logging.getLogger('mopidy.backends.libspotify.playback') +logger = logging.getLogger('mopidy.backends.spotify.playback') -class LibspotifyPlaybackProvider(BasePlaybackProvider): +class SpotifyPlaybackProvider(BasePlaybackProvider): def pause(self): return self.backend.output.set_state('PAUSED') diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/spotify/session_manager.py similarity index 87% rename from mopidy/backends/libspotify/session_manager.py rename to mopidy/backends/spotify/session_manager.py index f736a40e..6ec6b4c9 100644 --- a/mopidy/backends/libspotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -2,28 +2,29 @@ import logging import os import threading -from spotify.manager import SpotifySessionManager +import spotify.manager from mopidy import get_version, settings -from mopidy.backends.libspotify.translator import LibspotifyTranslator +from mopidy.backends.spotify.translator import SpotifyTranslator from mopidy.models import Playlist from mopidy.utils.process import BaseThread -logger = logging.getLogger('mopidy.backends.libspotify.session_manager') +logger = logging.getLogger('mopidy.backends.spotify.session_manager') # pylint: disable = R0901 -# LibspotifySessionManager: Too many ancestors (9/7) +# SpotifySessionManager: Too many ancestors (9/7) -class LibspotifySessionManager(SpotifySessionManager, BaseThread): +class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread): cache_location = settings.SPOTIFY_CACHE_PATH settings_location = settings.SPOTIFY_CACHE_PATH appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key') user_agent = 'Mopidy %s' % get_version() def __init__(self, username, password, core_queue, output): - SpotifySessionManager.__init__(self, username, password) + spotify.manager.SpotifySessionManager.__init__( + self, username, password) BaseThread.__init__(self, core_queue) - self.name = 'LibspotifySMThread' + self.name = 'SpotifySMThread' self.output = output self.connected = threading.Event() self.session = None @@ -53,7 +54,7 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread): playlists = [] for spotify_playlist in session.playlist_container(): playlists.append( - LibspotifyTranslator.to_mopidy_playlist(spotify_playlist)) + SpotifyTranslator.to_mopidy_playlist(spotify_playlist)) playlists = filter(None, playlists) self.core_queue.put({ 'command': 'set_stored_playlists', @@ -111,7 +112,7 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread): def callback(results, userdata=None): # TODO Include results from results.albums(), etc. too playlist = Playlist(tracks=[ - LibspotifyTranslator.to_mopidy_track(t) + SpotifyTranslator.to_mopidy_track(t) for t in results.tracks()]) connection.send(playlist) self.connected.wait() diff --git a/mopidy/backends/libspotify/spotify_appkey.key b/mopidy/backends/spotify/spotify_appkey.key similarity index 100% rename from mopidy/backends/libspotify/spotify_appkey.key rename to mopidy/backends/spotify/spotify_appkey.key diff --git a/mopidy/backends/libspotify/stored_playlists.py b/mopidy/backends/spotify/stored_playlists.py similarity index 84% rename from mopidy/backends/libspotify/stored_playlists.py rename to mopidy/backends/spotify/stored_playlists.py index 6f2a7aad..054e2bd1 100644 --- a/mopidy/backends/libspotify/stored_playlists.py +++ b/mopidy/backends/spotify/stored_playlists.py @@ -1,6 +1,6 @@ from mopidy.backends.base import BaseStoredPlaylistsProvider -class LibspotifyStoredPlaylistsProvider(BaseStoredPlaylistsProvider): +class SpotifyStoredPlaylistsProvider(BaseStoredPlaylistsProvider): def create(self, name): pass # TODO diff --git a/mopidy/backends/libspotify/translator.py b/mopidy/backends/spotify/translator.py similarity index 93% rename from mopidy/backends/libspotify/translator.py rename to mopidy/backends/spotify/translator.py index 4a42cf97..50ee07d1 100644 --- a/mopidy/backends/libspotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -4,12 +4,12 @@ import logging from spotify import Link, SpotifyError from mopidy import settings -from mopidy.backends.libspotify import ENCODING +from mopidy.backends.spotify import ENCODING from mopidy.models import Artist, Album, Track, Playlist -logger = logging.getLogger('mopidy.backends.libspotify.translator') +logger = logging.getLogger('mopidy.backends.spotify.translator') -class LibspotifyTranslator(object): +class SpotifyTranslator(object): @classmethod def to_mopidy_artist(cls, spotify_artist): if not spotify_artist.is_loaded(): diff --git a/mopidy/settings.py b/mopidy/settings.py index 1aaa4318..23aa7cb6 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -12,12 +12,12 @@ Available settings and their default values. #: #: Default:: #: -#: BACKENDS = (u'mopidy.backends.libspotify.LibspotifyBackend',) +#: BACKENDS = (u'mopidy.backends.spotify.SpotifyBackend',) #: #: .. note:: #: Currently only the first backend in the list is used. BACKENDS = ( - u'mopidy.backends.libspotify.LibspotifyBackend', + u'mopidy.backends.spotify.SpotifyBackend', ) #: The log format used for informational logging. @@ -169,24 +169,24 @@ MPD_SERVER_HOSTNAME = u'127.0.0.1' #: Default: 6600 MPD_SERVER_PORT = 6600 -#: Path to the libspotify cache. +#: Path to the Spotify cache. #: -#: Used by :mod:`mopidy.backends.libspotify`. -SPOTIFY_CACHE_PATH = u'~/.mopidy/libspotify_cache' +#: Used by :mod:`mopidy.backends.spotify`. +SPOTIFY_CACHE_PATH = u'~/.mopidy/spotify_cache' #: Your Spotify Premium username. #: -#: Used by :mod:`mopidy.backends.libspotify`. +#: Used by :mod:`mopidy.backends.spotify`. SPOTIFY_USERNAME = u'' #: Your Spotify Premium password. #: -#: Used by :mod:`mopidy.backends.libspotify`. +#: Used by :mod:`mopidy.backends.spotify`. SPOTIFY_PASSWORD = u'' #: Do you prefer high bitrate (320k)? #: -#: Used by :mod:`mopidy.backends.libspotify`. +#: Used by :mod:`mopidy.backends.spotify`. # #: Default:: #: From c2c39acbe87735954d7591a8b1e676f8994b6942 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 10 Jan 2011 17:03:54 +0100 Subject: [PATCH 198/235] Tweak log messages --- mopidy/backends/spotify/session_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 6ec6b4c9..c32148c3 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -37,10 +37,10 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread): logger.info(u'Connected to Spotify') self.session = session if settings.SPOTIFY_HIGH_BITRATE: - logger.debug(u'Prefer high bitrate') + logger.debug(u'Preferring high bitrate from Spotify') self.session.set_preferred_bitrate(1) else: - logger.debug(u'Prefer normal bitrate') + logger.debug(u'Preferring normal bitrate from Spotify') self.session.set_preferred_bitrate(0) self.connected.set() From feead97b5bf1fef8e1de6929538f0482eaef346e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 10 Jan 2011 22:13:06 +0100 Subject: [PATCH 199/235] Update reference to Spotify application key --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d77be3cd..d9d6af42 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( author='Stein Magnus Jodal', author_email='stein.magnus@jodal.no', packages=packages, - package_data={'mopidy': ['backends/libspotify/spotify_appkey.key']}, + package_data={'mopidy': ['backends/spotify/spotify_appkey.key']}, cmdclass=cmdclasses, data_files=data_files, scripts=['bin/mopidy', 'bin/mopidy-scan'], From 9f5e97d14be6b0063357e96e301318eabcf2cd09 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 10 Jan 2011 23:18:12 +0100 Subject: [PATCH 200/235] Make all tests pass without having pyserial installed --- mopidy/mixers/denon.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index e6d752b6..f0712f95 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -1,8 +1,6 @@ import logging from threading import Lock -from serial import Serial - from mopidy import settings from mopidy.mixers.base import BaseMixer @@ -33,8 +31,11 @@ class DenonMixer(BaseMixer): """ super(DenonMixer, self).__init__(*args, **kwargs) device = kwargs.get('device', None) - self._device = device or Serial(port=settings.MIXER_EXT_PORT, - timeout=0.2) + if device: + self._device = device + else: + from serial import Serial + self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2) self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] self._volume = 0 self._lock = Lock() From 65f8acb2873424ffee019ab546b2ebe0754b496e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 11 Jan 2011 22:09:51 +0100 Subject: [PATCH 201/235] Split Spotify playlist refreshing out of metadata updated callback --- mopidy/backends/spotify/session_manager.py | 26 +++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index c32148c3..9736f2eb 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -50,16 +50,8 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread): def metadata_updated(self, session): """Callback used by pyspotify""" - logger.debug(u'Metadata updated, refreshing stored playlists') - playlists = [] - for spotify_playlist in session.playlist_container(): - playlists.append( - SpotifyTranslator.to_mopidy_playlist(spotify_playlist)) - playlists = filter(None, playlists) - self.core_queue.put({ - 'command': 'set_stored_playlists', - 'playlists': playlists, - }) + logger.debug(u'Metadata updated') + self.refresh_stored_playlists() def connection_error(self, session, error): """Callback used by pyspotify""" @@ -107,6 +99,20 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread): logger.debug(u'End of data stream reached') self.output.end_of_data_stream() + def refresh_stored_playlists(self): + """Refresh the stored playlists in the backend with fresh meta data + from Spotify""" + playlists = [] + for spotify_playlist in self.session.playlist_container(): + playlists.append( + SpotifyTranslator.to_mopidy_playlist(spotify_playlist)) + playlists = filter(None, playlists) + self.core_queue.put({ + 'command': 'set_stored_playlists', + 'playlists': playlists, + }) + logger.debug(u'Refreshed %d stored playlist(s)', len(playlists)) + def search(self, query, connection): """Search method used by Mopidy backend""" def callback(results, userdata=None): From ed32b0a311827b8a388173261c0a9ea3ca377c5a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 13 Jan 2011 15:09:11 +0100 Subject: [PATCH 202/235] Update copyrights for 2011 --- docs/conf.py | 2 +- docs/licenses.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9e7ff1fb..9f5cedad 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,7 @@ master_doc = 'index' # General information about the project. project = u'Mopidy' -copyright = u'2010, Stein Magnus Jodal and contributors' +copyright = u'2010-2011, Stein Magnus Jodal and contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/licenses.rst b/docs/licenses.rst index c3a13904..7f4ed0ce 100644 --- a/docs/licenses.rst +++ b/docs/licenses.rst @@ -8,7 +8,7 @@ contributed what, please refer to our git repository. Source code license =================== -Copyright 2009-2010 Stein Magnus Jodal and contributors +Copyright 2009-2011 Stein Magnus Jodal and contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ limitations under the License. Documentation license ===================== -Copyright 2010 Stein Magnus Jodal and contributors +Copyright 2010-2011 Stein Magnus Jodal and contributors This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License. To view a copy of this license, visit From 3ce953ff103c3a9019b069886bb501f7ab667a41 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 13 Jan 2011 15:57:18 +0100 Subject: [PATCH 203/235] docs: Add textual logo to make it easier to get to the front page --- docs/_static/mopidy.png | Bin 0 -> 3382 bytes docs/conf.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/_static/mopidy.png diff --git a/docs/_static/mopidy.png b/docs/_static/mopidy.png new file mode 100644 index 0000000000000000000000000000000000000000..7d6ce5af106ca298fcafaa087857f8a0fed83a75 GIT binary patch literal 3382 zcmV-64axF}P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipM+ z4mJUgqC3a{000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}000b;Nklq{mz_!-ght0 zKD+z>Fn;{u?E zBTXOR+^DLC#l^rP`x_V45s?MU#4qxr(L!ig++C)ZbBeGKb`d+}tb!W2Gw`-hf4?{u zGGjo_=_2ZjD}k|j6T(n2|7C)?nbrgvB%~V}`#&?*=bSF0z8DBR2HXVfofjbt5%UiQ z&d#|(gMf=X>W|2VzvrATqW!1YuIgW*G~9P{wt1!NC*+1 zB7_bM0e)fUNi99Trp(*-QcTVgaB|89?IxNzmA`n&B@#kJZ9*u=7+^NAt%@#3bR_Cl zC1VeaV^SJi4BVR{--ST$6!`|)Hf+nN(C)wq!12InU_Xr0QwwHOZ4JhwZxQf-C9k}& zGceMw;rF*|_Pv4KG0s4J?f%SG%w?a~fz`lL;9+acH9GoR)AY-B@C~-@{utO`+pq+< z4|Az0B7ZyuoQ~Oup8^%?|Rg?$u4iQ z)O#vs!)_(;Nk)A?XZLTiZ0Z*EMIHo9^V}O+o>HC{ggk#t(T4d#x&P?|7DDAmXjNIB zJ(3b1nsI>s$+Iz+WUOywN+q5I{5M0r8%V#AS}NeT%6@?xg%Bp&C0(hYUnQ;)@~rdR z=jtMUtZwXNgaGgxw)+vaBqcm9V@G{rO>PA}Fvnb3>Ps^TuFP>@-jrf9wWvw*3GmAX zAcXJNSgRIE2uF+gzTnZ0!^AQtbwWazW7A*jN_bihoi$Yy>IXU0b*iWGt}S42-rgXD z(D9pAM?#nwfDB4CzUFI%d{=nxi7Ds#y*e2o^v2vx*?@50;e0^}iyi;pY&VjU&{DCE zsrFkW$ns;)wR=$m^qdAF1mByec@aV%%rRD}4uk;k2O;0n9(q|Il((>xoHCT-w>IsW zO5s-sN;pYaTV3p^=S18`ieD3Cxm;A*FibeGq6X*(z-JqX5I&&nI4KYzObgVuTJoGA zm<(sOM2f{wu{boVua&fH=?|0LEJj zrynK)&0I0>;v6U7k@m!=moLywqgm9_&bw1ruPQQ%u+$Q9GSZL4XLOCmF45Zmql<|Y;?Kq`?sU{GaM{f8e1{WRw35x$}adeVS_iT zJR^ksZGs$m$kPoi{N9-kD@9rU9Mx=uQ0@HO#hm4#v>pUrcBHS2mGC2yz3To3JXq(~egTAuF((8)I7E+O413I7}K z*#83K)b<2ThBSmw-l^@t?;PpRC@ZrU?vQI!66&kP=1EXosTX+Y>>(jds4vGBajA1; z0}#R^0mt=BAuUJ5+6Zxhl~ z^Kv~%7&99%L4=x~5R_}r)h6h4N1pxdQsCf(G@EO5Vw)gSlH9uq`PX^qObJK6Tfjbi zMMyKKU>VYmn<8;1|GxoV4aZd>v&ll)^DtMpo1PGqhfk_)Vm*!9CG>ou12?7|NJ(-l ze&(A!bf%>39k37E8bBU}5IzVzw%--Z`c~ju6@UAxQ1)D!6ln;d)&QKIL5`#gQCSHg z?OsZjv>`W}J`~cl7c7Gki=k&7#`n!an$s(Y&Gm-C2Fx9s9fA=03VCWBbIl?B>plxH zo_9X=sd6h{rky-wRnl$=*oVs3e`5oXQlC1E@5Q(yN{wavb^S?Hk0Mr%dxfRQ4n+uU z36tQClycmV(Iifidu`5ZY-N97ZNNTMCfbIAWl;8DhSX_{D_6lgoRq#Uq@76e2w;LQs94YTi%e-O5=HJMLVlkRSc(_hlL7lsag#1O zlJQeQn#qJceJRI{nDBB*P#!IA>g7UFgs{L`3JQKNl$Vtd7UA9m8ZBB0ql9$rgmcp! zAQDv3gs@Qfa`q*^H$Z*yGTN=rBq-jGMWUx&9`26&o<&eXaP6R%aNlnLa|vZy4)wX@ zZbLgaz6wY5S>RVfIrH&zbjpIC31;paz&{I?<4v^SFh2(lsMCbHE@S?BSO_QAxHo2tdM9Er>B6;rO!3~Hl^^OUV*f71+`23z9>RTs0EdfZ z-AX>;u*V28%@fLbMC|+pc2e(R&D?RAwE$ST>MhXdLI)CZj5bH3+7JB9(G(5MF_{IB!v3{TLwQNeXPk`2|;=F`Ez1fqa3WJ zVx%CGa^vqn(!C$4{XtA5DYCV_KVKZ@gN}aq# zwl_XM1?7>A0fCjXt00q)XJA*%rI)-0<~;mg?kRGjv?D&iQQ#(CSIkLMBN74_7iFc6 zj|h8}y#tq~?Dtdh7DS2SyHjB0>@04;`Qj|HJ>2pk=GZTdIa$;sS(|cD6g2_Ai8=Oc zG(rIFA==s02R(tuJaq9MpT5SO3$_ZCcMYtZZfIe((_jzGIqzl<<$r{^#IxRm1mH6m@5ffV z1UwJzMW>R@9}y7=p_AirvY%eejcSaDgwXlX9kbb*g$&*yyyIO&L?na)a5UMyJHN7{ zE3r!)M-feeEItG6zz@+b$wfp&3!xKWeoD(=69swsMMOkGs2`rke~B;`a~ct05zkud zM+mWR@?ZWQg^93r7$*M3o@~Dhvk`NxcsVA*&>T$Yw}^;{h=^+VAGt#boVk+cC;$Ke M07*qoM6N<$f^NJ)I{*Lx literal 0 HcmV?d00001 diff --git a/docs/conf.py b/docs/conf.py index 9f5cedad..dc5adecd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -116,7 +116,7 @@ html_theme_path = ['_themes'] # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +html_logo = '_static/mopidy.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 From 79bd041924ba5a5233870ee3f62d4514ea375fe6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 13 Jan 2011 15:57:49 +0100 Subject: [PATCH 204/235] docs: Add 'this page' source link to make it easier to understand how the page is made and thus contribute fixes --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index dc5adecd..4587c16d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -153,7 +153,7 @@ html_last_updated_fmt = '%b %d, %Y' #html_split_index = False # If true, links to the reST sources are added to the pages. -html_show_sourcelink = False +#html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the From 4eb694c84bbab57ecf6148f72a9cf1ac9512b084 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Jan 2011 12:51:20 +0100 Subject: [PATCH 205/235] Catch and log pylast.WSError --- mopidy/frontends/lastfm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index 60c2d708..e2d58b1e 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -103,7 +103,7 @@ class LastfmFrontendThread(BaseThread): duration=str(duration), track_number=str(track.track_no), mbid=(track.musicbrainz_id or '')) - except (pylast.ScrobblingError, socket.error) as e: + except (pylast.ScrobblingError, pylast.WSError, socket.error) as e: logger.warning(u'Last.fm now playing error: %s', e) def stopped_playing(self, track, stop_position): @@ -129,5 +129,5 @@ class LastfmFrontendThread(BaseThread): track_number=str(track.track_no), duration=str(duration), mbid=(track.musicbrainz_id or '')) - except (pylast.ScrobblingError, socket.error) as e: + except (pylast.ScrobblingError, pylast.WSError, socket.error) as e: logger.warning(u'Last.fm scrobbling error: %s', e) From e7ed28d0d45a4631f9c808caa55252ab9eec59be Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Jan 2011 14:00:35 +0100 Subject: [PATCH 206/235] Fix crash in NadMixer when using unicode strings for settings --- docs/changes.rst | 5 +++++ mopidy/mixers/nad.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index ddb46bb8..dcbb5893 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -71,6 +71,11 @@ No description yet. any help from the original MPD server. - Support UTF-8 encoded tag caches with non-ASCII characters. +- Mixers: + + - Support use of unicode strings for :mod:`mopidy.mixers.nad` specific + settings. + - Models: - Rename and generalize ``Playlist._with(**kwargs)`` to diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index 3215a761..5cf92826 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -147,6 +147,8 @@ class NadTalker(BaseThread): return self._readline().replace('%s=' % key, '') def _command_device(self, key, value): + if type(value) == unicode: + value = value.encode('utf-8') self._write('%s=%s' % (key, value)) self._readline() From 222aac6352834b8f1cc22dbbb325407edd3ff5f8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 19 Jan 2011 15:10:27 +0100 Subject: [PATCH 207/235] docs: Add section on impdclient --- docs/clients/mpd.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index de54dfcb..4761ccfc 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -40,7 +40,6 @@ If you run Ubuntu 10.04 or older, you can fetch an updated version of ncmpcpp from `Launchpad `_. - Graphical clients ================= @@ -92,6 +91,14 @@ yet. iPhone/iPod Touch clients ========================= +impdclient +---------- + +There's an open source MPD client for iOS called `impdclient +`_ which has not seen any updates since +August 2008. So far, we've not heard of users trying it with Mopidy. Please +notify us of your successes and/or problems if you do try it out. + MPod ---- From 03d533099349c795e4d3f4687d018078ab77203d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 19 Jan 2011 15:10:50 +0100 Subject: [PATCH 208/235] docs: Add results of MPoD testing --- docs/clients/mpd.rst | 46 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index 4761ccfc..dd0e885c 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -102,4 +102,48 @@ notify us of your successes and/or problems if you do try it out. MPod ---- -Works well with Mopidy as far as we've heard from users. +The `MPoD `_ client can be +installed from the `iTunes Store +`_. + +Users have reported varying success in using MPoD together with Mopidy. Thus, +we've tested a fresh install of MPoD 1.5.1 with Mopidy as of revision e7ed28d +(pre-0.3) on an iPod Touch 3rd generation. The following are our findings: + +- **Works:** Playback control generally works, including stop, play, pause, + previous, next, repeat, random, seek, and volume control. + +- **Bug:** Search does not work, neither in the artist, album, or song + tabs. Mopidy gets no requests at all from MPoD when executing searches. Seems + like MPoD only searches in local cache, even if "Use local cache" is turned + off in MPoD's settings. Until this is fixed by the MPoD developer, MPoD will + be much less useful with Mopidy. + +- **Bug:** When adding another playlist to the current playlist in MPoD, + the currently playing track restarts at the beginning. I do not currently + know enough about this bug, because I'm not sure if MPoD was in the "add to + active playlist" or "replace active playlist" mode when I tested it. I only + later learned what that button was for. Anyway, what I experienced was: + + #. I play a track + #. I select a new playlist + #. MPoD reconnects to Mopidy for unknown reason + #. MPoD issues MPD command ``load "a playlist name"`` + #. MPoD issues MPD command ``play "-1"`` + #. MPoD issues MPD command ``playlistinfo "-1"`` + #. I hear that the currently playing tracks restarts playback + +- **Tips:** MPoD seems to cache stored playlists, but they won't work if the + server hasn't loaded stored playlists from e.g. Spotify yet. A trick to force + refetching of playlists from Mopidy is to add a new empty playlist in MPoD. + +- **Wishlist:** Modifying the current playlists is not supported by MPoD it + seems. + +- **Wishlist:** MPoD supports playback of Last.fm radio streams through the MPD + server. Mopidy does not currently support this, but there is a wishlist bug + at :issue:`38`. + +- **Wishlist:** MPoD supports autodetection/-configuration of MPD servers + through the use of Bonjour. Mopidy does not currently support this, but there + is a wishlist bug at :issue:`39`. From 7179754ec257b863578e3aea8727010d3516a203 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 19 Jan 2011 23:30:48 +0100 Subject: [PATCH 209/235] docs: Convert roadmap to wishlist issues --- docs/development/roadmap.rst | 107 +++++++++-------------------------- 1 file changed, 26 insertions(+), 81 deletions(-) diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst index 9db74a4d..cec8e9c7 100644 --- a/docs/development/roadmap.rst +++ b/docs/development/roadmap.rst @@ -2,88 +2,33 @@ Roadmap ******* -This is the current roadmap and collection of wild ideas for future Mopidy -development. This is intended to be a living document and may change at any -time. -We intend to have about one timeboxed release every month. Thus, the roadmap is -oriented around "soon" and "later" instead of mapping each feature to a future -release. +Release schedule +================ + +We intend to have about one timeboxed feature release every month +in periods of active development. The feature releases are numbered 0.x.0. The +features added is a mix of what we feel is most important/requested of the +missing features, and features we develop just because we find them fun to +make, even though they may be useful for very few users or for a limited use +case. + +Bugfix releases, numbered 0.x.y, will be released whenever we discover bugs +that are too serious to wait for the next feature release. We will only release +bugfix releases for the last feature release. E.g. when 0.3.0 is released, we +will no longer provide bugfix releases for the 0.2 series. In other words, +there will be just a single supported release at any point in time. -Possible targets for the next version -===================================== +Feature wishlist +================ -- Reintroduce support for OS X. See :issue:`25` for details. -- **[WIP: feature/multi-backend]** Support for using multiple Mopidy backends - simultaneously. Should make it possible to have both Spotify tracks and local - tracks in the same playlist. -- MPD frontend: - - - **[WIP: feature/mpd-password]** Password authentication. - - ``idle`` support. - -- Spotify backend: - - - Write-support for Spotify, i.e. playlist management. - - Virtual directories with e.g. starred tracks from Spotify. - - **[DONE: v0.3]** Support for 320 kbps audio. - -- Local backend: - - - Better music library support. - - **[DONE: v0.3]** A script for creating a tag cache. - - An alternative to tag cache for caching metadata, i.e. Sqlite. - -- **[DONE: v0.2]** Last.fm scrobbling. - - -Stuff we want to do, but not right now, and maybe never -======================================================= - -- Packaging and distribution: - - - **[BLOCKED]** Create `Homebrew `_ - recipies for all our dependencies and Mopidy itself to make OS X - installation a breeze. See `Homebrew's issue #1612 - `_. - - **[DONE]** Create `Debian packages - `_ of all our dependencies and - Mopidy itself (hosted in our own Debian repo until we get stuff into the - various distros) to make Debian/Ubuntu installation a breeze. - -- Compatability: - - - **[WIP: feature/blackbox-testing]** Run frontend tests against a real MPD - server to ensure we are in sync. - -- Backends: - - - `Last.fm `_ - - `WIMP `_ - - DNLA/UPnP so Mopidy can play music from other DNLA MediaServers. - -- Frontends: - - - Publish the server's presence to the network using `Zeroconf - `_/Avahi. - - **[WIP: feature/mpris-frontend]** D-Bus/`MPRIS `_ - - **[WIP: feature/http-frontend]** REST/JSON web service with a jQuery client - as example application. Maybe based upon `Tornado - `_ and `jQuery - Mobile `_. - - DNLA/UPnP so Mopidy can be controlled from i.e. TVs. - - `XMMS2 `_ - - LIRC frontend for controlling Mopidy with a remote. - -- Mixers: - - - LIRC mixer for controlling arbitrary amplifiers remotely. - -- Audio streaming: - - - Ogg Vorbis/MP3 audio stream over HTTP, to MPD clients, `Squeezeboxes - `_, etc. - - Feed audio to an `Icecast `_ server. - - Stream to AirPort Express using `RAOP - `_. +We maintain our collection of sane or less sane ideas for future Mopidy +features as `issues `_ at GitHub +labeled with `the "wishlist" label +`_. Feel free to vote +up any feature you would love to see in Mopidy, but please refrain from adding +a comment just to say "I want this too!". You are of course free to add +comments if you have suggestions for how the feature should work or be +implemented, and you may add new wishlist issues if your ideas are not already +represented. From 970ed5d7b2a26a2cdc319b94229e9f6cf4897fab Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Jan 2011 00:10:36 +0100 Subject: [PATCH 210/235] docs: Add warning to changelog on the Spotify playlist loading issue --- docs/changes.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index dcbb5893..86137c1a 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -8,8 +8,18 @@ This change log is used to track all major changes to Mopidy. 0.3.0 (in development) ====================== +.. warning:: Known bug in Spotify playlist loading + + There is a known bug in the loading of Spotify playlists. This bug affects + both Mopidy 0.2.1 and 0.3.0, given that you use libspotify 0.0.6. To avoid + the bug, either use Mopidy 0.2.1 with libspotify 0.0.4, or use either + Mopidy version with libspotify 0.0.6 and follow the simple workaround + described at :issue:`59`. + + No description yet. + **Important changes** - Spotify backend: From b187e934409a482d6dfbd0c7f9bdf0ff230f479d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Jan 2011 14:52:24 +0100 Subject: [PATCH 211/235] docs: Elaborate on issues with the graphical clients --- docs/clients/mpd.rst | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index dd0e885c..f95b83e7 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -20,7 +20,8 @@ ncmpc ----- A console client. Uses the ``idle`` command heavily, which Mopidy doesn't -support yet. If you want a console client, use ncmpcpp instead. +support yet (see :issue:`32`). If you want a console client, use ncmpcpp +instead. ncmpcpp ------- @@ -46,16 +47,35 @@ Graphical clients GMPC ---- -A GTK+ client which works well with Mopidy, and is regularly used by Mopidy -developers. +`GMPC `_ is a graphical MPD client (GTK+) which works +well with Mopidy, and is regularly used by Mopidy developers. + +GMPC may sometimes requests a lot of meta data of related albums, artists, etc. +This takes more time with Mopidy, which needs to query Spotify for the data, +than with a normal MPD server, which has a local cache of meta data. Thus, GMPC +may sometimes feel frozen, but usually you just need to give it a bit of slack +before it will catch up. Sonata ------ -A GTK+ client. Generally works well with Mopidy. +`Sonata `_ is a graphical MPD client (GTK+). +It generally works well with Mopidy, except for search. -Search does not work, because they do most of the search on the client side. -See :issue:`1` for details. +When you search in Sonata, it only sends the first to letters of the search +query to Mopidy, and then does the rest of the filtering itself on the client +side. Since Spotify has a collection of millions of tracks and they only return +the first 100 hits for any search query, searching for two-letter combinations +seldom returns any useful results. See :issue:`1` and the matching `Sonata +bug`_ for details. + +.. _Sonata bug: http://developer.berlios.de/feature/?func=detailfeature&feature_id=5038&group_id=7323 + +Theremin +-------- + +`Theremin `_ is a graphical MPD client for OS X. +It generally works well with Mopidy. Android clients From 2992a81ce0090b690523fca72d0e2b3a06c6bb1d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Jan 2011 17:05:32 +0100 Subject: [PATCH 212/235] Fix crash in lastfm frontend on tracks that does not contain the expected data --- docs/changes.rst | 2 ++ mopidy/frontends/lastfm.py | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 86137c1a..326ae4af 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -46,6 +46,8 @@ No description yet. - Update to use Last.fm's new Scrobbling 2.0 API, as the old Submissions Protocol 1.2.1 is deprecated. (Fixes: :issue:`33`) + - Fix crash when track object does not contain all the expected meta data. + **Changes** diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index e2d58b1e..69d1c8bc 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -92,14 +92,14 @@ class LastfmFrontendThread(BaseThread): def started_playing(self, track): artists = ', '.join([a.name for a in track.artists]) - duration = track.length // 1000 + duration = track.length and track.length // 1000 or 0 self.last_start_time = int(time.time()) logger.debug(u'Now playing track: %s - %s', artists, track.name) try: self.lastfm.update_now_playing( artists, - track.name, - album=track.album.name, + (track.name or ''), + album=(track.album and track.album.name or ''), duration=str(duration), track_number=str(track.track_no), mbid=(track.musicbrainz_id or '')) @@ -108,7 +108,7 @@ class LastfmFrontendThread(BaseThread): def stopped_playing(self, track, stop_position): artists = ', '.join([a.name for a in track.artists]) - duration = track.length // 1000 + duration = track.length and track.length // 1000 or 0 stop_position = stop_position // 1000 if duration < 30: logger.debug(u'Track too short to scrobble. (30s)') @@ -123,9 +123,9 @@ class LastfmFrontendThread(BaseThread): try: self.lastfm.scrobble( artists, - track.name, + (track.name or ''), str(self.last_start_time), - album=track.album.name, + album=(track.album and track.album.name or ''), track_number=str(track.track_no), duration=str(duration), mbid=(track.musicbrainz_id or '')) From a0f8106e7a5ba7a6cecde943aacd8e172a478ec6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Jan 2011 18:11:27 +0100 Subject: [PATCH 213/235] docs: Review six Android MPD clients --- docs/clients/mpd.rst | 151 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 143 insertions(+), 8 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index f95b83e7..6ecfded5 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -16,6 +16,7 @@ mpc A command line client. Version 0.14 had some issues with Mopidy (see :issue:`5`), but 0.16 seems to work nicely. + ncmpc ----- @@ -23,6 +24,7 @@ A console client. Uses the ``idle`` command heavily, which Mopidy doesn't support yet (see :issue:`32`). If you want a console client, use ncmpcpp instead. + ncmpcpp ------- @@ -56,6 +58,7 @@ than with a normal MPD server, which has a local cache of meta data. Thus, GMPC may sometimes feel frozen, but usually you just need to give it a bit of slack before it will catch up. + Sonata ------ @@ -71,6 +74,7 @@ bug`_ for details. .. _Sonata bug: http://developer.berlios.de/feature/?func=detailfeature&feature_id=5038&group_id=7323 + Theremin -------- @@ -81,31 +85,161 @@ It generally works well with Mopidy. Android clients =============== +We've tested all six MPD clients we could find for Android with Mopidy 0.3 on a +HTC Hero with Android 2.1, using the following test procedure: + +#. Connect to Mopidy +#. Search for ``foo``, with search type "any" if it can be selected +#. Add "The Pretender" from the search results to the current playlist +#. Start playback +#. Pause and resume playback +#. Adjust volume +#. Find a playlist and append it to the current playlist +#. Skip to next track +#. Skip to previous track +#. Select the last track from the current playlist +#. Turn on repeat mode +#. Seek to 10 seconds or so before the end of the track +#. Wait for the end of the track and confirm that playback continues at the + start of the playlist +#. Turn off repeat mode +#. Turn on random mode +#. Skip to next track and confirm that it random mode works +#. Turn off random mode +#. Stop playback +#. Check if the app got support for single mode and consume mode +#. Kill Mopidy and confirm that the app handles it without crashing + +In summary: + +- BitMPC lacks finishing touches on its user interface but supports all + features tested. +- Droid MPD Client works well, but got a couple of bugs one can live with and + does not expose stored playlist anywhere. +- IcyBeats is not usable yet. +- MPDroid is working well and looking good, but does not have search + functionality. +- PMix is just a lesser MPDroid, so use MPDroid instead. +- ThreeMPD is too buggy to even get connected to Mopidy. + +Our recommendation: + +- If you do not care about looks, use BitMPC. +- If you do not care about stored playlists, use Droid MPD Client. +- If you do not care about searching, use MPDroid. + + BitMPC ------ -Works well with Mopidy. +We tested version 1.0.0, which at the time had 1k-5k downloads, <100 ratings, +3.5 stars. -Droid MPD ---------- +The user interface lacks some finishing touches. E.g. you can't enter a +hostname for the server. Only IPv4 addresses are allowed. + +All features exercised in the test procedure works. BitMPC lacks support for +single mode and consume mode. BitMPC crashes if Mopidy is killed or crash. + + +Droid MPD Client +---------------- + +We tested version 0.4.0, which at the time had 5k-10k downloads, >200 ratings, +4 stars. + +To find the search functionality, you have to select the menu, then "Playlist +manager", then the search tab. I do not understand why search is hidden inside +"Playlist manager". + +The user interface have some French remnants, like "Rechercher" in the search +field. + +When selecting the artist tab, it issues the ``list Artist`` command and +becomes stuck waiting for the results. Same thing happens for the album tab, +which issues ``list Album``, and the folder tab, which issues ``lsinfo``. +Mopidy returned zero hits immediately on all three commands. If Mopidy has +loaded your stored playlists and returns more than zero hits on these commands, +they artist and album tabs do not hang. The folder tab still freezes when +``lsinfo`` returns a list of stored playlists, though zero files. Thus, we've +discovered a couple of bugs in Droid MPD Client. + +The volume control is very slick, with a turn knob, just like on an amplifier. +It lends itself to showing off to friends when combined with Mopidy's external +amplifier mixers. Everybody loves turning a knob on a touch screen and see the +physical knob on the amplifier turn as well ;-) + +Even though ``lsinfo`` returns the stored playlists for the folder tab, they +are not displayed anywhere. Thus, we had to select an album in the album tab to +complete the test procedure. + +At one point, I had problems turning off repeat mode. After I adjusted the +volume and tried again, it worked. + +Droid MPD client does not support single mode or consume mode. It does not +detect that the server is killed/crashed. You'll only notice it by no actions +having any effect, e.g. you can't turn the volume knob any more. + +In conclusion, some bugs and caveats, but most of the test procedure was +possible to perform. + + +IcyBeats +-------- + +We tested version 0.2, which at the time had 50-100 downloads, no ratings. +The app was still in beta when we tried it. + +IcyBeats successfully connected to Mopidy and I was able to adjust volume. When +I was searching for some tracks, I could not figure out how to actually start +the search, as there was no search button and pressing enter in the input field +just added a new line. I was stuck. In other words, IcyBeats 0.2 is not usable +with Mopidy. + +IcyBeats does have something going for it: IcyBeats uses IPv6 to connect to +Mopidy. The future is just around the corner! -Works well with Mopidy. MPDroid ------- -Works well with Mopidy, and is regularly used by Mopidy developers. +We tested version 0.6.9, which at the time had 5k-10k downloads, <200 ratings, +4.5 stars. MPDroid started out as a fork of PMix. + +First of all, MPDroid's user interface looks nice. + +I couldn't find any search functionality, so I added the initial track using +another client. Other than the missing search functionality, everything in the +test procedure worked out flawlessly. Like all other Android clients, MPDroid +does not support single mode or consume mode. When Mopidy is killed, MPDroid +handles it gracefully and asks if you want to try to reconnect. + +All in all, MPDroid is a good MPD client without search support. + PMix ---- -Works well with Mopidy. +We tested version 0.4.0, which at the time had 10k-50k downloads, >200 ratings, +4 stars. + +Add MPDroid is a fork from PMix, it is no surprise that PMix does not support +search either. In addition, I could not find stored playlists. Other than that, +I was able to complete the test procedure. PMix crashed once during testing, +but handled the killing of Mopidy just as nicely as MPDroid. It does not +support single mode or consume mode. + +All in all, PMix works but can do less than MPDroid. Use MPDroid instead. + ThreeMPD -------- -Does not work well with Mopidy, because we haven't implemented ``listallinfo`` -yet. +We tested version 0.3.0, which at the time had 1k-5k downloads, <25 ratings, +2.5 average. The developer request users to use MPDroid instead, due to limited +time for maintenance. Does not support password authentication. + +ThreeMPD froze during startup, so we were not able to test it. iPhone/iPod Touch clients @@ -119,6 +253,7 @@ There's an open source MPD client for iOS called `impdclient August 2008. So far, we've not heard of users trying it with Mopidy. Please notify us of your successes and/or problems if you do try it out. + MPod ---- From d75d0388c5f130817efcdd39b27ee0e643f165d0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Jan 2011 18:33:51 +0100 Subject: [PATCH 214/235] docs: Copy most of README.rst into the docs to avoid linking from the develop docs to the master docs --- README.rst | 15 ++++++++------- docs/index.rst | 29 ++++++++++++++++++++++++++++- docs/installation/index.rst | 2 ++ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 4f31fb59..c063de79 100644 --- a/README.rst +++ b/README.rst @@ -6,14 +6,15 @@ Mopidy is a music server which can play music from `Spotify `_ or from your local hard drive. To search for music in Spotify's vast archive, manage playlists, and play music, you can use most `MPD clients `_. MPD clients are available for most -platforms, including Windows, Mac OS X, Linux, and iPhone and Android phones. +platforms, including Windows, Mac OS X, Linux, Android and iOS. To install Mopidy, check out `the installation docs `_. -* `Documentation (latest release) `_ -* `Documentation (development version) `_ -* `Source code `_ -* `Issue tracker `_ -* IRC: ``#mopidy`` at `irc.freenode.net `_ -* `Download development snapshot `_ +- `Documentation for the latest release `_ +- `Documentation for the development version + `_ +- `Source code `_ +- `Issue tracker `_ +- IRC: ``#mopidy`` at `irc.freenode.net `_ +- `Download development snapshot `_ diff --git a/docs/index.rst b/docs/index.rst index 09029a4f..0af45835 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,31 @@ -.. include:: ../README.rst +****** +Mopidy +****** + +Mopidy is a music server which can play music from `Spotify +`_ or from your local hard drive. To search for music +in Spotify's vast archive, manage playlists, and play music, you can use most +`MPD clients `_. MPD clients are available for most +platforms, including Windows, Mac OS X, Linux, Android, and iOS. + +To install Mopidy, start out by reading :ref:`installation`. + +If you get stuck, we usually hang around at ``#mopidy`` at `irc.freenode.net +`_. If you stumble into a bug or got a feature request, +please create an issue in the `issue tracker +`_. + + +Project resources +================= + +- `Documentation for the latest release `_ +- `Documentation for the development version + `_ +- `Source code `_ +- `Issue tracker `_ +- IRC: ``#mopidy`` at `irc.freenode.net `_ + User documentation ================== diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 26b50994..56f0015b 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -1,3 +1,5 @@ +.. _installation: + ************ Installation ************ From 8f0e00e1d781b60b2bb5cb2f6c850ff2627eb5ce Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 00:17:01 +0100 Subject: [PATCH 215/235] Implement 'password' command which will be reached post-auth --- mopidy/frontends/mpd/exceptions.py | 5 +++++ mopidy/frontends/mpd/protocol/connection.py | 9 +++++++-- tests/frontends/mpd/connection_test.py | 19 +++++++++++++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpd/exceptions.py b/mopidy/frontends/mpd/exceptions.py index 2a18b2f3..53a31dd9 100644 --- a/mopidy/frontends/mpd/exceptions.py +++ b/mopidy/frontends/mpd/exceptions.py @@ -39,6 +39,11 @@ class MpdArgError(MpdAckError): super(MpdArgError, self).__init__(*args, **kwargs) self.error_code = 2 # ACK_ERROR_ARG +class MpdPasswordError(MpdAckError): + def __init__(self, *args, **kwargs): + super(MpdPasswordError, self).__init__(*args, **kwargs) + self.error_code = 3 # ACK_ERROR_PASSWORD + class MpdUnknownCommand(MpdAckError): def __init__(self, *args, **kwargs): super(MpdUnknownCommand, self).__init__(*args, **kwargs) diff --git a/mopidy/frontends/mpd/protocol/connection.py b/mopidy/frontends/mpd/protocol/connection.py index 0ce3ef51..65811d09 100644 --- a/mopidy/frontends/mpd/protocol/connection.py +++ b/mopidy/frontends/mpd/protocol/connection.py @@ -1,5 +1,6 @@ +from mopidy import settings from mopidy.frontends.mpd.protocol import handle_pattern -from mopidy.frontends.mpd.exceptions import MpdNotImplemented +from mopidy.frontends.mpd.exceptions import MpdPasswordError @handle_pattern(r'^close$') def close(frontend): @@ -33,7 +34,11 @@ def password_(frontend, password): This is used for authentication with the server. ``PASSWORD`` is simply the plaintext password. """ - raise MpdNotImplemented # TODO + # You will not get to this code without being authenticated. This is for + # when you are already authenticated, and are sending additional 'password' + # requests. + if settings.MPD_SERVER_PASSWORD != password: + raise MpdPasswordError(u'incorrect password', command=u'password') @handle_pattern(r'^ping$') def ping(frontend): diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py index 21753054..e7bb74de 100644 --- a/tests/frontends/mpd/connection_test.py +++ b/tests/frontends/mpd/connection_test.py @@ -1,5 +1,6 @@ import unittest +from mopidy import settings from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer @@ -9,6 +10,9 @@ class ConnectionHandlerTest(unittest.TestCase): self.b = DummyBackend(mixer_class=DummyMixer) self.h = dispatcher.MpdDispatcher(backend=self.b) + def tearDown(self): + settings.runtime.clear() + def test_close(self): result = self.h.handle_request(u'close') self.assert_(u'OK' in result) @@ -21,9 +25,20 @@ class ConnectionHandlerTest(unittest.TestCase): result = self.h.handle_request(u'kill') self.assert_(u'OK' in result) - def test_password(self): + def test_valid_password_is_accepted(self): + settings.MPD_SERVER_PASSWORD = u'topsecret' + result = self.h.handle_request(u'password "topsecret"') + self.assert_(u'OK' in result) + + def test_invalid_password_is_not_accepted(self): + settings.MPD_SERVER_PASSWORD = u'topsecret' result = self.h.handle_request(u'password "secret"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) + self.assert_(u'ACK [3@0] {password} incorrect password' in result) + + def test_any_password_is_not_accepted_when_password_check_turned_off(self): + settings.MPD_SERVER_PASSWORD = False + result = self.h.handle_request(u'password "secret"') + self.assert_(u'ACK [3@0] {password} incorrect password' in result) def test_ping(self): result = self.h.handle_request(u'ping') From af0f80d2385001b52560cb0ec9d31e85c4a891bf Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 00:17:30 +0100 Subject: [PATCH 216/235] Add MPD_SERVER_PASSWORD to list of relevant frontend settings --- mopidy/frontends/mpd/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index ce9abc6d..2f87088c 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -14,6 +14,7 @@ class MpdFrontend(BaseFrontend): **Settings:** - :attr:`mopidy.settings.MPD_SERVER_HOSTNAME` + - :attr:`mopidy.settings.MPD_SERVER_PASSWORD` - :attr:`mopidy.settings.MPD_SERVER_PORT` """ From 00897e90240fb80d1697979ddec3fa7032c29e6e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 00:18:04 +0100 Subject: [PATCH 217/235] Improve MPD_SERVER_PASSWORD description --- mopidy/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/settings.py b/mopidy/settings.py index b2ee5b12..add32428 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -164,7 +164,7 @@ OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput' #: Listens on all interfaces, both IPv4 and IPv6. MPD_SERVER_HOSTNAME = u'127.0.0.1' -#: Password required for connecting to the MPD server. +#: The password required for connecting to the MPD server. #: #: Default: :class:`False`, which means no password required. MPD_SERVER_PASSWORD = False From 775ec649763f77bd869d203e5de7f9cfe1697e5b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 02:15:27 +0100 Subject: [PATCH 218/235] Test and improve password handling in MpdSession --- mopidy/frontends/mpd/session.py | 32 ++++++++++++------ tests/frontends/mpd/server_test.py | 53 ++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py index 3ad80c92..b67e7f13 100644 --- a/mopidy/frontends/mpd/session.py +++ b/mopidy/frontends/mpd/session.py @@ -49,8 +49,10 @@ class MpdSession(asynchat.async_chat): def handle_request(self, request): """Handle request by sending it to the MPD frontend.""" if not self.authenticated: - self.authenticated = self.check_password(request) - return + (self.authenticated, response) = self.check_password(request) + if response is not None: + self.send_response(response) + return my_end, other_end = multiprocessing.Pipe() self.core_queue.put({ 'to': 'frontend', @@ -76,13 +78,23 @@ class MpdSession(asynchat.async_chat): self.push(data) def check_password(self, request): + """ + Takes any request and tries to authenticate the client using it. + + :rtype: a two-tuple containing (is_authenticated, response_message). If + the response_message is :class:`None`, normal processing should + continue, even though the client may not be authenticated. + """ if not settings.MPD_SERVER_PASSWORD: - return True - if request == 'password "%s"' % settings.MPD_SERVER_PASSWORD: - self.send_response('OK') - return True + return (True, None) command = request.split(' ')[0] - self.send_response( - "ACK [4@0] {%s} " % command + - "you don't have permission for \"%s\"" % command) - return False + if command == 'password': + if request == 'password "%s"' % settings.MPD_SERVER_PASSWORD: + return (True, u'OK') + else: + return (False, u'ACK [3@0] {password} incorrect password') + if command in ('close', 'commands', 'notcommands', 'ping'): + return (False, None) + else: + return (False, "ACK [4@0] {%s} " % command + + "you don't have permission for \"%s\"" % command) diff --git a/tests/frontends/mpd/server_test.py b/tests/frontends/mpd/server_test.py index 9d006eb3..9d877ed5 100644 --- a/tests/frontends/mpd/server_test.py +++ b/tests/frontends/mpd/server_test.py @@ -1,5 +1,6 @@ import unittest +from mopidy import settings from mopidy.frontends.mpd import server class MpdServerTest(unittest.TestCase): @@ -21,8 +22,60 @@ class MpdSessionTest(unittest.TestCase): def setUp(self): self.session = server.MpdSession(None, None, (None, None), None) + def tearDown(self): + settings.runtime.clear() + def test_found_terminator_catches_decode_error(self): # Pressing Ctrl+C in a telnet session sends a 0xff byte to the server. self.session.input_buffer = ['\xff'] self.session.found_terminator() self.assertEqual(len(self.session.input_buffer), 0) + + def test_authentication_with_valid_password_is_accepted(self): + settings.MPD_SERVER_PASSWORD = u'topsecret' + authed, response = self.session.check_password(u'password "topsecret"') + self.assertTrue(authed) + self.assertEqual(u'OK', response) + + def test_authentication_with_invalid_password_is_not_accepted(self): + settings.MPD_SERVER_PASSWORD = u'topsecret' + authed, response = self.session.check_password(u'password "secret"') + self.assertFalse(authed) + self.assertEqual(u'ACK [3@0] {password} incorrect password', response) + + def test_authentication_with_anything_when_password_check_turned_off(self): + settings.MPD_SERVER_PASSWORD = False + authed, response = self.session.check_password(u'any request at all') + self.assertTrue(authed) + self.assertEqual(None, response) + + def test_anything_when_not_authenticated_should_fail(self): + settings.MPD_SERVER_PASSWORD = u'topsecret' + authed, response = self.session.check_password(u'any request at all') + self.assertFalse(authed) + self.assertEqual( + u'ACK [4@0] {any} you don\'t have permission for "any"', response) + + def test_close_is_allowed_without_authentication(self): + settings.MPD_SERVER_PASSWORD = u'topsecret' + authed, response = self.session.check_password(u'close') + self.assertFalse(authed) + self.assertEqual(None, response) + + def test_commands_is_allowed_without_authentication(self): + settings.MPD_SERVER_PASSWORD = u'topsecret' + authed, response = self.session.check_password(u'commands') + self.assertFalse(authed) + self.assertEqual(None, response) + + def test_notcommands_is_allowed_without_authentication(self): + settings.MPD_SERVER_PASSWORD = u'topsecret' + authed, response = self.session.check_password(u'notcommands') + self.assertFalse(authed) + self.assertEqual(None, response) + + def test_ping_is_allowed_without_authentication(self): + settings.MPD_SERVER_PASSWORD = u'topsecret' + authed, response = self.session.check_password(u'ping') + self.assertFalse(authed) + self.assertEqual(None, response) From 4953a80cce16a75e3802889dddb0586980c8ffa3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 02:20:07 +0100 Subject: [PATCH 219/235] Add FIXME's to 'commands' and 'notcommands', as they currently do not care if the client is authenticated or not --- mopidy/frontends/mpd/protocol/reflection.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py index d2c9c599..83efdd94 100644 --- a/mopidy/frontends/mpd/protocol/reflection.py +++ b/mopidy/frontends/mpd/protocol/reflection.py @@ -9,9 +9,12 @@ def commands(frontend): ``commands`` Shows which commands the current user has access to. - - As permissions is not implemented, any user has access to all commands. """ + # FIXME When password auth is turned on and the client is not + # authenticated, 'commands' should list only the commands the client does + # have access to. To implement this we need access to the session object to + # check if the client is authenticated or not. + sorted_commands = sorted(list(mpd_commands)) # Not shown by MPD in its command list @@ -51,9 +54,11 @@ def notcommands(frontend): ``notcommands`` Shows which commands the current user does not have access to. - - As permissions is not implemented, any user has access to all commands. """ + # FIXME When password auth is turned on and the client is not + # authenticated, 'notcommands' should list all the commands the client does + # not have access to. To implement this we need access to the session + # object to check if the client is authenticated or not. pass @handle_pattern(r'^tagtypes$') From c85292ab5fd8516ac4d3f02301ac5fe88ed7feea Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 02:26:33 +0100 Subject: [PATCH 220/235] docs: Add section on how to use MPD_SERVER_PASSWORD --- docs/settings.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/settings.rst b/docs/settings.rst index 532f52cf..9adbe76b 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -95,6 +95,13 @@ 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`. +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. + Scrobbling tracks to Last.fm ============================ From bf46b73b646920c0db4ce842fdc8f7337ef0a2e1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 16:05:04 +0100 Subject: [PATCH 221/235] Fix typo in variable name --- tests/utils/settings_test.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index cef0069d..f7a95eaa 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -69,34 +69,34 @@ class SettingsProxyTest(unittest.TestCase): def test_value_ending_in_path_is_expanded(self): self.settings.TEST_PATH = '~/test' - acctual = self.settings.TEST_PATH + actual = self.settings.TEST_PATH expected = os.path.expanduser('~/test') - self.assertEqual(acctual, expected) + self.assertEqual(actual, expected) def test_value_ending_in_path_is_absolute(self): self.settings.TEST_PATH = './test' - acctual = self.settings.TEST_PATH + actual = self.settings.TEST_PATH expected = os.path.abspath('./test') - self.assertEqual(acctual, expected) + self.assertEqual(actual, expected) def test_value_ending_in_file_is_expanded(self): self.settings.TEST_FILE = '~/test' - acctual = self.settings.TEST_FILE + actual = self.settings.TEST_FILE expected = os.path.expanduser('~/test') - self.assertEqual(acctual, expected) + self.assertEqual(actual, expected) def test_value_ending_in_file_is_absolute(self): self.settings.TEST_FILE = './test' - acctual = self.settings.TEST_FILE + actual = self.settings.TEST_FILE expected = os.path.abspath('./test') - self.assertEqual(acctual, expected) + self.assertEqual(actual, expected) def test_value_not_ending_in_path_or_file_is_not_expanded(self): self.settings.TEST = '~/test' - acctual = self.settings.TEST - self.assertEqual(acctual, '~/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' - acctual = self.settings.TEST - self.assertEqual(acctual, './test') + actual = self.settings.TEST + self.assertEqual(actual, './test') From 7f4ce3be8aa3eea5d3785891ed4f19443e2f41b2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 16:29:04 +0100 Subject: [PATCH 222/235] Fix SettingsProxy.__getattr__ to support settings that are None or 0. --- mopidy/utils/settings.py | 2 +- tests/utils/settings_test.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 2ec0f716..7715721e 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -49,7 +49,7 @@ class SettingsProxy(object): if attr not in self.current: raise SettingsError(u'Setting "%s" is not set.' % attr) value = self.current[attr] - if type(value) != bool and not value: + if isinstance(value, basestring) and len(value) == 0: raise SettingsError(u'Setting "%s" is empty.' % attr) if attr.endswith('_PATH') or attr.endswith('_FILE'): value = os.path.expanduser(value) diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index f7a95eaa..8e2575b9 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -1,7 +1,7 @@ import os import unittest -from mopidy import settings as default_settings_module +from mopidy import settings as default_settings_module, SettingsError from mopidy.utils.settings import validate_settings, SettingsProxy class ValidateSettingsTest(unittest.TestCase): @@ -55,6 +55,33 @@ class SettingsProxyTest(unittest.TestCase): self.settings.TEST = 'test' self.assertEqual(self.settings.TEST, 'test') + def test_getattr_raises_error_on_missing_setting(self): + try: + test = self.settings.TEST + self.fail(u'Should raise exception') + except SettingsError as e: + self.assertEqual(u'Setting "TEST" is not set.', e.message) + + def test_getattr_raises_error_on_empty_setting(self): + self.settings.TEST = u'' + try: + test = self.settings.TEST + self.fail(u'Should raise exception') + except SettingsError as e: + self.assertEqual(u'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.assert_('TEST' in self.settings.runtime) From e1197ed84c4fc6667ea6d44725951164fd768021 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 16:31:35 +0100 Subject: [PATCH 223/235] Change default value of MPD_SERVER_PASSWORD from False to None --- mopidy/frontends/mpd/session.py | 2 +- mopidy/settings.py | 4 ++-- tests/frontends/mpd/connection_test.py | 2 +- tests/frontends/mpd/server_test.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py index b67e7f13..1161dac5 100644 --- a/mopidy/frontends/mpd/session.py +++ b/mopidy/frontends/mpd/session.py @@ -85,7 +85,7 @@ class MpdSession(asynchat.async_chat): the response_message is :class:`None`, normal processing should continue, even though the client may not be authenticated. """ - if not settings.MPD_SERVER_PASSWORD: + if settings.MPD_SERVER_PASSWORD is None: return (True, None) command = request.split(' ')[0] if command == 'password': diff --git a/mopidy/settings.py b/mopidy/settings.py index add32428..6e33ffaa 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -166,8 +166,8 @@ MPD_SERVER_HOSTNAME = u'127.0.0.1' #: The password required for connecting to the MPD server. #: -#: Default: :class:`False`, which means no password required. -MPD_SERVER_PASSWORD = False +#: Default: :class:`None`, which means no password required. +MPD_SERVER_PASSWORD = None #: Which TCP port Mopidy's MPD server should listen to. #: diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py index e7bb74de..44ce78ca 100644 --- a/tests/frontends/mpd/connection_test.py +++ b/tests/frontends/mpd/connection_test.py @@ -36,7 +36,7 @@ class ConnectionHandlerTest(unittest.TestCase): self.assert_(u'ACK [3@0] {password} incorrect password' in result) def test_any_password_is_not_accepted_when_password_check_turned_off(self): - settings.MPD_SERVER_PASSWORD = False + settings.MPD_SERVER_PASSWORD = None result = self.h.handle_request(u'password "secret"') self.assert_(u'ACK [3@0] {password} incorrect password' in result) diff --git a/tests/frontends/mpd/server_test.py b/tests/frontends/mpd/server_test.py index 9d877ed5..48c7e790 100644 --- a/tests/frontends/mpd/server_test.py +++ b/tests/frontends/mpd/server_test.py @@ -44,7 +44,7 @@ class MpdSessionTest(unittest.TestCase): self.assertEqual(u'ACK [3@0] {password} incorrect password', response) def test_authentication_with_anything_when_password_check_turned_off(self): - settings.MPD_SERVER_PASSWORD = False + settings.MPD_SERVER_PASSWORD = None authed, response = self.session.check_password(u'any request at all') self.assertTrue(authed) self.assertEqual(None, response) From 55b32065a8dd32c5d3fa6660916c5f63fbd1a201 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 16:38:16 +0100 Subject: [PATCH 224/235] Expose MPD error codes as constants on the MpdAckError exception, to avoid use of magic values in the other exceptions --- mopidy/frontends/mpd/exceptions.py | 36 ++++++++++++++---------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/mopidy/frontends/mpd/exceptions.py b/mopidy/frontends/mpd/exceptions.py index 53a31dd9..faf4ce2f 100644 --- a/mopidy/frontends/mpd/exceptions.py +++ b/mopidy/frontends/mpd/exceptions.py @@ -1,22 +1,20 @@ from mopidy import MopidyException class MpdAckError(MopidyException): - """ - Available MPD error codes:: + """See fields on this class for available MPD error codes""" - ACK_ERROR_NOT_LIST = 1 - ACK_ERROR_ARG = 2 - ACK_ERROR_PASSWORD = 3 - ACK_ERROR_PERMISSION = 4 - ACK_ERROR_UNKNOWN = 5 - ACK_ERROR_NO_EXIST = 50 - ACK_ERROR_PLAYLIST_MAX = 51 - ACK_ERROR_SYSTEM = 52 - ACK_ERROR_PLAYLIST_LOAD = 53 - ACK_ERROR_UPDATE_ALREADY = 54 - ACK_ERROR_PLAYER_SYNC = 55 - ACK_ERROR_EXIST = 56 - """ + ACK_ERROR_NOT_LIST = 1 + ACK_ERROR_ARG = 2 + ACK_ERROR_PASSWORD = 3 + ACK_ERROR_PERMISSION = 4 + ACK_ERROR_UNKNOWN = 5 + ACK_ERROR_NO_EXIST = 50 + ACK_ERROR_PLAYLIST_MAX = 51 + ACK_ERROR_SYSTEM = 52 + ACK_ERROR_PLAYLIST_LOAD = 53 + ACK_ERROR_UPDATE_ALREADY = 54 + ACK_ERROR_PLAYER_SYNC = 55 + ACK_ERROR_EXIST = 56 def __init__(self, message=u'', error_code=0, index=0, command=u''): super(MpdAckError, self).__init__(message, error_code, index, command) @@ -37,24 +35,24 @@ class MpdAckError(MopidyException): class MpdArgError(MpdAckError): def __init__(self, *args, **kwargs): super(MpdArgError, self).__init__(*args, **kwargs) - self.error_code = 2 # ACK_ERROR_ARG + self.error_code = MpdAckError.ACK_ERROR_ARG class MpdPasswordError(MpdAckError): def __init__(self, *args, **kwargs): super(MpdPasswordError, self).__init__(*args, **kwargs) - self.error_code = 3 # ACK_ERROR_PASSWORD + self.error_code = MpdAckError.ACK_ERROR_PASSWORD class MpdUnknownCommand(MpdAckError): def __init__(self, *args, **kwargs): super(MpdUnknownCommand, self).__init__(*args, **kwargs) self.message = u'unknown command "%s"' % self.command self.command = u'' - self.error_code = 5 # ACK_ERROR_UNKNOWN + self.error_code = MpdAckError.ACK_ERROR_UNKNOWN class MpdNoExistError(MpdAckError): def __init__(self, *args, **kwargs): super(MpdNoExistError, self).__init__(*args, **kwargs) - self.error_code = 50 # ACK_ERROR_NO_EXIST + self.error_code = MpdAckError.ACK_ERROR_NO_EXIST class MpdNotImplemented(MpdAckError): def __init__(self, *args, **kwargs): From 1625c1e08022f856c2581262c094d5fdea843282 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 16:44:24 +0100 Subject: [PATCH 225/235] Be consistent in use of unicode string --- mopidy/frontends/mpd/session.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py index 1161dac5..e8e3291d 100644 --- a/mopidy/frontends/mpd/session.py +++ b/mopidy/frontends/mpd/session.py @@ -96,5 +96,6 @@ class MpdSession(asynchat.async_chat): if command in ('close', 'commands', 'notcommands', 'ping'): return (False, None) else: - return (False, "ACK [4@0] {%s} " % command + - "you don't have permission for \"%s\"" % command) + return (False, + u'ACK [4@0] {%(c)s} you don\'t have permission for "%(c)s"' % + {'c': command}) From 142a16aacca5ad62bae0ca04224f78c5c560da1c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 16:50:38 +0100 Subject: [PATCH 226/235] docs: Add MPD password auth to changelog --- docs/changes.rst | 3 +++ docs/settings.rst | 2 ++ 2 files changed, 5 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 326ae4af..e8d1a88f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -72,6 +72,9 @@ No description yet. - MPD frontend: + - Add support for password authentication. See + :attr:`mopidy.settings.MPD_SERVER_PASSWORD` and + :ref:`use_mpd_on_a_network` for details on how to use it. - Support ``setvol 50`` without quotes around the argument. Fixes volume control in Droid MPD. - Support ``seek 1 120`` without quotes around the arguments. Fixes seek in diff --git a/docs/settings.rst b/docs/settings.rst index 9adbe76b..8ac83e92 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -88,6 +88,8 @@ To make a ``tag_cache`` of your local music available for Mopidy: #. Start Mopidy, find the music library in a client, and play some local music! +.. _use_mpd_on_a_network: + Connecting from other machines on the network ============================================= From bfb5c45ebe11f2322b34c17668b741c8f1ada992 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 21:55:09 +0100 Subject: [PATCH 227/235] Cleanup changelog --- docs/changes.rst | 135 +++++++++++++++++++++++++--------------------- docs/settings.rst | 10 ++-- 2 files changed, 80 insertions(+), 65 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index e8d1a88f..f27a45c9 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -17,19 +17,23 @@ This change log is used to track all major changes to Mopidy. described at :issue:`59`. -No description yet. - - **Important changes** +- If you use the Spotify backend, you need to upgrade to libspotify 0.0.6 and + the latest pyspotify from the Mopidy developers. Follow the instructions at + :doc:`/installation/libspotify/`. + +- If you use the Last.fm frontend, you need to upgrade to pylast 0.5. Run + ``sudp pip install --upgrade pylast`` or install Mopidy from APT. + + +**Changes** + - Spotify backend: - - If you use the Spotify backend, you need to upgrade to libspotify 0.0.6 and - the latest pyspotify from the Mopidy developers. Follow the instructions at - :doc:`/installation/libspotify/`. - - - Support high bitrate (320k) audio. See - :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE` for details. + - Support high bitrate (320k) audio. Set the new setting + :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE` to :class:`True` to switch to + high bitrate audio. - Rename :mod:`mopidy.backends.libspotify` to :mod:`mopidy.backends.spotify`. If you have set :attr:`mopidy.settings.BACKENDS` explicitly, you may need @@ -37,88 +41,97 @@ No description yet. - Catch and log error caused by playlist folder boundaries being threated as normal playlists. More permanent fix requires support for checking playlist - types in pyspotify. + types in pyspotify (see :issue:`62`). + +- Local backend: + + - Add :command:`mopidy-scan` command to generate ``tag_cache`` files without + any help from the original MPD server. See :ref:`generating_a_tag_cache` + for instructions on how to use it. + + - Fix support for UTF-8 encoding in tag caches. + +- MPD frontend: + + - Add support for password authentication. See + :attr:`mopidy.settings.MPD_SERVER_PASSWORD` and + :ref:`use_mpd_on_a_network` for details on how to use it. (Fixes: + :issue:`41`) + + - Support ``setvol 50`` without quotes around the argument. Fixes volume + control in Droid MPD. + + - Support ``seek 1 120`` without quotes around the arguments. Fixes seek in + Droid MPD. - Last.fm frontend: - - If you use the Last.fm frontend, you need to upgrade to pylast 0.5. - - Update to use Last.fm's new Scrobbling 2.0 API, as the old Submissions Protocol 1.2.1 is deprecated. (Fixes: :issue:`33`) - Fix crash when track object does not contain all the expected meta data. +- Mixers: -**Changes** + - Support use of unicode strings for settings specific to + :mod:`mopidy.mixers.nad`. - Settings: - - Automatically expand ``~`` to the user's home directory and make the path - absolute for settings with names ending in ``_PATH`` or ``_FILE``. + - Automatically expand the "~" characted to the user's home directory and + make the path absolute for settings with names ending in ``_PATH`` or + ``_FILE``. + - Rename the following settings. The settings validator will warn you if you need to change your local settings. - - ``LOCAL_MUSIC_FOLDER`` to :attr:`mopidy.settings.LOCAL_MUSIC_PATH` - - ``LOCAL_PLAYLIST_FOLDER`` to - :attr:`mopidy.settings.LOCAL_PLAYLIST_PATH` - - ``LOCAL_TAG_CACHE`` to :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE` - - ``SPOTIFY_LIB_CACHE`` to :attr:`mopidy.settings.SPOTIFY_CACHE_PATH` + - ``LOCAL_MUSIC_FOLDER`` to :attr:`mopidy.settings.LOCAL_MUSIC_PATH` + - ``LOCAL_PLAYLIST_FOLDER`` to + :attr:`mopidy.settings.LOCAL_PLAYLIST_PATH` + - ``LOCAL_TAG_CACHE`` to :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE` + - ``SPOTIFY_LIB_CACHE`` to :attr:`mopidy.settings.SPOTIFY_CACHE_PATH` + + - Fix bug which made settings set to :class:`None` or 0 cause a + :exc:`mopidy.SettingsError` to be raised. - Packaging and distribution: + - Setup APT repository and crate Debian packages of Mopidy. See + :ref:`installation` for instructions for how to install Mopidy, including + all dependencies, from APT. + - Install ``mopidy.desktop`` file that makes Mopidy available from e.g. Gnome application menus. - - Create infrastructure for creating Debian packages of Mopidy. -- MPD frontend: - - - Add support for password authentication. See - :attr:`mopidy.settings.MPD_SERVER_PASSWORD` and - :ref:`use_mpd_on_a_network` for details on how to use it. - - Support ``setvol 50`` without quotes around the argument. Fixes volume - control in Droid MPD. - - Support ``seek 1 120`` without quotes around the arguments. Fixes seek in - Droid MPD. - -- Local backend: - - - Add :command:`mopidy-scan` command to generate ``tag_cache`` files without - any help from the original MPD server. - - Support UTF-8 encoded tag caches with non-ASCII characters. - -- Mixers: - - - Support use of unicode strings for :mod:`mopidy.mixers.nad` specific - settings. - -- Models: +- API: - Rename and generalize ``Playlist._with(**kwargs)`` to :meth:`mopidy.models.ImmutableObject.copy`. + - Add ``musicbrainz_id`` field to :class:`mopidy.models.Artist`, :class:`mopidy.models.Album`, and :class:`mopidy.models.Track`. -- Introduce the :ref:`provider concept `. Split the backend - API into a :ref:`backend controller API ` (for - frontend use) and a :ref:`backend provider API ` (for - backend implementation use), which includes the following changes: + - Prepare for multi-backend support (see :issue:`40`) by introducing the + :ref:`provider concept `. Split the backend API into a + :ref:`backend controller API ` (for frontend use) + and a :ref:`backend provider API ` (for backend + implementation use), which includes the following changes: - - Rename ``BaseBackend`` to :class:`mopidy.backends.base.Backend`. - - Rename ``BaseCurrentPlaylistController`` to - :class:`mopidy.backends.base.CurrentPlaylistController`. - - Split ``BaseLibraryController`` to - :class:`mopidy.backends.base.LibraryController` and - :class:`mopidy.backends.base.BaseLibraryProvider`. - - Split ``BasePlaybackController`` to - :class:`mopidy.backends.base.PlaybackController` and - :class:`mopidy.backends.base.BasePlaybackProvider`. - - Split ``BaseStoredPlaylistsController`` to - :class:`mopidy.backends.base.StoredPlaylistsController` and - :class:`mopidy.backends.base.BaseStoredPlaylistsProvider`. - -- Other API and package structure cleaning: + - Rename ``BaseBackend`` to :class:`mopidy.backends.base.Backend`. + - Rename ``BaseCurrentPlaylistController`` to + :class:`mopidy.backends.base.CurrentPlaylistController`. + - Split ``BaseLibraryController`` to + :class:`mopidy.backends.base.LibraryController` and + :class:`mopidy.backends.base.BaseLibraryProvider`. + - Split ``BasePlaybackController`` to + :class:`mopidy.backends.base.PlaybackController` and + :class:`mopidy.backends.base.BasePlaybackProvider`. + - Split ``BaseStoredPlaylistsController`` to + :class:`mopidy.backends.base.StoredPlaylistsController` and + :class:`mopidy.backends.base.BaseStoredPlaylistsProvider`. - Move ``BaseMixer`` to :class:`mopidy.mixers.base.BaseMixer`. + - Add docs for the current non-stable output API, :class:`mopidy.outputs.base.BaseOutput`. diff --git a/docs/settings.rst b/docs/settings.rst index 8ac83e92..1d4a4972 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -59,11 +59,13 @@ You may also want to change some of the ``LOCAL_*`` settings. See hopefully have support for this in the 0.3 release. +.. _generating_a_tag_cache: + Generating a tag cache ---------------------- -Previously the local storage backend relied purely on ``tag_cache`` files -generated by the original MPD server. To remedy this the command +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` has been created. The program will scan your current :attr:`mopidy.settings.LOCAL_MUSIC_PATH` and build a MPD compatible ``tag_cache``. @@ -82,8 +84,8 @@ To make a ``tag_cache`` of your local music available for Mopidy: 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. + :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE` is set to, or change the + setting to point to where your ``tag_cache`` file is. #. Start Mopidy, find the music library in a client, and play some local music! From be09a8a62a1f5d6fe7876c7fe343392a8fdb6905 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 22:45:31 +0100 Subject: [PATCH 228/235] Catch exceptions thrown by pylast on connection problems --- docs/changes.rst | 7 +++++++ mopidy/frontends/lastfm.py | 25 +++++++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index f27a45c9..c0d04102 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -71,6 +71,13 @@ This change log is used to track all major changes to Mopidy. - Fix crash when track object does not contain all the expected meta data. + - Fix crash when response from Last.fm cannot be decoded as UTF-8. (Fixes: + :issue:`37`) + + - Fix crash when response from Last.fm contains invalid XML. + + - Fix crash when response from Last.fm has an invalid HTTP status line. + - Mixers: - Support use of unicode strings for settings specific to diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index 69d1c8bc..62b08174 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -1,7 +1,9 @@ +from httplib import HTTPException import logging import multiprocessing import socket import time +from xml.parsers.expat import ExpatError try: import pylast @@ -9,7 +11,7 @@ except ImportError as import_error: from mopidy import OptionalDependencyError raise OptionalDependencyError(import_error) -from mopidy import get_version, settings, SettingsError +from mopidy import settings, SettingsError from mopidy.frontends.base import BaseFrontend from mopidy.utils.process import BaseThread @@ -54,6 +56,18 @@ class LastfmFrontend(BaseFrontend): class LastfmFrontendThread(BaseThread): + # Whenever we call pylast, we catch the following non-pylast exceptions, as + # they are not caught and wrapped by pylast. + # + # socket.error: + # Not reported upstream. + # UnicodeDecodeError: + # http://code.google.com/p/pylast/issues/detail?id=55 + # xml.parsers.expat.ExpatError: + # http://code.google.com/p/pylast/issues/detail?id=58 + # httplib.HTTPException: + # Not reported upstream. + def __init__(self, core_queue, connection): super(LastfmFrontendThread, self).__init__(core_queue) self.name = u'LastfmFrontendThread' @@ -79,7 +93,8 @@ class LastfmFrontendThread(BaseThread): except SettingsError as e: logger.info(u'Last.fm scrobbler not started') logger.debug(u'Last.fm settings error: %s', e) - except (pylast.WSError, socket.error) as e: + except (pylast.WSError, socket.error, UnicodeDecodeError, ExpatError, + HTTPException) as e: logger.error(u'Last.fm connection error: %s', e) def process_message(self, message): @@ -103,7 +118,8 @@ class LastfmFrontendThread(BaseThread): duration=str(duration), track_number=str(track.track_no), mbid=(track.musicbrainz_id or '')) - except (pylast.ScrobblingError, pylast.WSError, socket.error) as e: + except (pylast.ScrobblingError, pylast.WSError, socket.error, + UnicodeDecodeError, ExpatError, HTTPException) as e: logger.warning(u'Last.fm now playing error: %s', e) def stopped_playing(self, track, stop_position): @@ -129,5 +145,6 @@ class LastfmFrontendThread(BaseThread): track_number=str(track.track_no), duration=str(duration), mbid=(track.musicbrainz_id or '')) - except (pylast.ScrobblingError, pylast.WSError, socket.error) as e: + except (pylast.ScrobblingError, pylast.WSError, socket.error, + UnicodeDecodeError, ExpatError, HTTPException) as e: logger.warning(u'Last.fm scrobbling error: %s', e) From 3f49cda5d238d1e4f3107a6242f80e178269212e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 23:00:47 +0100 Subject: [PATCH 229/235] Add description of the 0.3.0 release --- docs/changes.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index c0d04102..e5fb958e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -8,6 +8,16 @@ This change log is used to track all major changes to Mopidy. 0.3.0 (in development) ====================== +Mopidy 0.3.0 brings a bunch of small changes all over the place, but no large +changes. The main features are support for high bitrate audio from Spotify, and +MPD password authentication. + +Please note that 0.3.0 requires some updated dependencies, as listed under +*Important changes* below. Also, there is a known bug in the Spotify playlist +loading, as described below. As the bug will take some time to fix and has a +known workaround, we did not want to delay the release while waiting for a fix +to this problem. + .. warning:: Known bug in Spotify playlist loading There is a known bug in the loading of Spotify playlists. This bug affects From f28a62b4a297ea48f5973dac8088a3f3f9c93309 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 23:55:55 +0100 Subject: [PATCH 230/235] Update Last.fm frontend to catch new exceptions thrown by pylast 0.5.7 --- docs/changes.rst | 2 +- mopidy/frontends/lastfm.py | 35 ++++++++++------------------------- requirements/lastfm.txt | 2 +- 3 files changed, 12 insertions(+), 27 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index e5fb958e..dcaf7b2b 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -33,7 +33,7 @@ to this problem. the latest pyspotify from the Mopidy developers. Follow the instructions at :doc:`/installation/libspotify/`. -- If you use the Last.fm frontend, you need to upgrade to pylast 0.5. Run +- If you use the Last.fm frontend, you need to upgrade to pylast 0.5.7. Run ``sudp pip install --upgrade pylast`` or install Mopidy from APT. diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index 62b08174..d2c9af88 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -1,9 +1,6 @@ -from httplib import HTTPException import logging import multiprocessing -import socket import time -from xml.parsers.expat import ExpatError try: import pylast @@ -31,7 +28,7 @@ class LastfmFrontend(BaseFrontend): **Dependencies:** - - `pylast `_ >= 0.5 + - `pylast `_ >= 0.5.7 **Settings:** @@ -56,18 +53,6 @@ class LastfmFrontend(BaseFrontend): class LastfmFrontendThread(BaseThread): - # Whenever we call pylast, we catch the following non-pylast exceptions, as - # they are not caught and wrapped by pylast. - # - # socket.error: - # Not reported upstream. - # UnicodeDecodeError: - # http://code.google.com/p/pylast/issues/detail?id=55 - # xml.parsers.expat.ExpatError: - # http://code.google.com/p/pylast/issues/detail?id=58 - # httplib.HTTPException: - # Not reported upstream. - def __init__(self, core_queue, connection): super(LastfmFrontendThread, self).__init__(core_queue) self.name = u'LastfmFrontendThread' @@ -93,9 +78,9 @@ class LastfmFrontendThread(BaseThread): except SettingsError as e: logger.info(u'Last.fm scrobbler not started') logger.debug(u'Last.fm settings error: %s', e) - except (pylast.WSError, socket.error, UnicodeDecodeError, ExpatError, - HTTPException) as e: - logger.error(u'Last.fm connection error: %s', e) + except (pylast.NetworkError, pylast.MalformedResponseError, + pylast.WSError) as e: + logger.error(u'Error during Last.fm setup: %s', e) def process_message(self, message): if message['command'] == 'started_playing': @@ -118,9 +103,9 @@ class LastfmFrontendThread(BaseThread): duration=str(duration), track_number=str(track.track_no), mbid=(track.musicbrainz_id or '')) - except (pylast.ScrobblingError, pylast.WSError, socket.error, - UnicodeDecodeError, ExpatError, HTTPException) as e: - logger.warning(u'Last.fm now playing error: %s', e) + except (pylast.ScrobblingError, pylast.NetworkError, + pylast.MalformedResponseError, pylast.WSError) as e: + logger.warning(u'Error submitting playing track to Last.fm: %s', e) def stopped_playing(self, track, stop_position): artists = ', '.join([a.name for a in track.artists]) @@ -145,6 +130,6 @@ class LastfmFrontendThread(BaseThread): track_number=str(track.track_no), duration=str(duration), mbid=(track.musicbrainz_id or '')) - except (pylast.ScrobblingError, pylast.WSError, socket.error, - UnicodeDecodeError, ExpatError, HTTPException) as e: - logger.warning(u'Last.fm scrobbling error: %s', e) + except (pylast.ScrobblingError, pylast.NetworkError, + pylast.MalformedResponseError, pylast.WSError) as e: + logger.warning(u'Error submitting played track to Last.fm: %s', e) diff --git a/requirements/lastfm.txt b/requirements/lastfm.txt index 887a0f0d..314c4223 100644 --- a/requirements/lastfm.txt +++ b/requirements/lastfm.txt @@ -1 +1 @@ -pylast >= 0.5 +pylast >= 0.5.7 From 62111c277cdd38854609527d1c1abdb4b8b96e45 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Jan 2011 23:57:38 +0100 Subject: [PATCH 231/235] Remove int to string conversions needed for pylast < 0.5.7 --- mopidy/frontends/lastfm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index d2c9af88..52786373 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -100,8 +100,8 @@ class LastfmFrontendThread(BaseThread): artists, (track.name or ''), album=(track.album and track.album.name or ''), - duration=str(duration), - track_number=str(track.track_no), + duration=duration, + track_number=track.track_no, mbid=(track.musicbrainz_id or '')) except (pylast.ScrobblingError, pylast.NetworkError, pylast.MalformedResponseError, pylast.WSError) as e: @@ -125,10 +125,10 @@ class LastfmFrontendThread(BaseThread): self.lastfm.scrobble( artists, (track.name or ''), - str(self.last_start_time), + self.last_start_time, album=(track.album and track.album.name or ''), - track_number=str(track.track_no), - duration=str(duration), + track_number=track.track_no, + duration=duration, mbid=(track.musicbrainz_id or '')) except (pylast.ScrobblingError, pylast.NetworkError, pylast.MalformedResponseError, pylast.WSError) as e: From 5d4c13268f2e93eb638fc6c4af2805b293ee6cbc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 22 Jan 2011 00:10:52 +0100 Subject: [PATCH 232/235] Revert "Remove int to string conversions needed for pylast < 0.5.7" This reverts commit 62111c277cdd38854609527d1c1abdb4b8b96e45. The issue is not fully fixed in pylast yet. --- mopidy/frontends/lastfm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index 52786373..d2c9af88 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -100,8 +100,8 @@ class LastfmFrontendThread(BaseThread): artists, (track.name or ''), album=(track.album and track.album.name or ''), - duration=duration, - track_number=track.track_no, + duration=str(duration), + track_number=str(track.track_no), mbid=(track.musicbrainz_id or '')) except (pylast.ScrobblingError, pylast.NetworkError, pylast.MalformedResponseError, pylast.WSError) as e: @@ -125,10 +125,10 @@ class LastfmFrontendThread(BaseThread): self.lastfm.scrobble( artists, (track.name or ''), - self.last_start_time, + str(self.last_start_time), album=(track.album and track.album.name or ''), - track_number=track.track_no, - duration=duration, + track_number=str(track.track_no), + duration=str(duration), mbid=(track.musicbrainz_id or '')) except (pylast.ScrobblingError, pylast.NetworkError, pylast.MalformedResponseError, pylast.WSError) as e: From 2a1a3a8f0d70c779c67063d52ee922fad20dfe58 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 22 Jan 2011 00:43:45 +0100 Subject: [PATCH 233/235] Fix crash on failed lookup of URI from Spotify --- docs/changes.rst | 2 ++ mopidy/backends/spotify/library.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index dcaf7b2b..cd24b345 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -53,6 +53,8 @@ to this problem. normal playlists. More permanent fix requires support for checking playlist types in pyspotify (see :issue:`62`). + - Fix crash on failed lookup of track by URI. (Fixes: :issue:`60`) + - Local backend: - Add :command:`mopidy-scan` command to generate ``tag_cache`` files without diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index 16391473..5e2f66ae 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -22,7 +22,7 @@ class SpotifyLibraryProvider(BaseLibraryProvider): # playlists. return SpotifyTranslator.to_mopidy_track(spotify_track) except SpotifyError as e: - logger.warning(u'Failed to lookup: %s', uri, e) + logger.debug(u'Failed to lookup "%s": %s', uri, e) return None def refresh(self, uri=None): From a7aefea0ab7b4f68068ff5c9c73caf086284ffa5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 22 Jan 2011 13:55:47 +0100 Subject: [PATCH 234/235] docs: Link from changelog to MPC client reviews --- docs/changes.rst | 5 +++++ docs/clients/mpd.rst | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index cd24b345..68955c3d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,12 +12,17 @@ Mopidy 0.3.0 brings a bunch of small changes all over the place, but no large changes. The main features are support for high bitrate audio from Spotify, and MPD password authentication. +Regarding the docs, we've improved the :ref:`installation instructions +` and done a bit of testing of the available :ref:`Android +` and :ref:`iOS clients ` for MPD. + Please note that 0.3.0 requires some updated dependencies, as listed under *Important changes* below. Also, there is a known bug in the Spotify playlist loading, as described below. As the bug will take some time to fix and has a known workaround, we did not want to delay the release while waiting for a fix to this problem. + .. warning:: Known bug in Spotify playlist loading There is a known bug in the loading of Spotify playlists. This bug affects diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index 6ecfded5..e27aa446 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -82,6 +82,8 @@ Theremin It generally works well with Mopidy. +.. _android_mpd_clients: + Android clients =============== @@ -242,6 +244,8 @@ time for maintenance. Does not support password authentication. ThreeMPD froze during startup, so we were not able to test it. +.. _ios_mpd_clients: + iPhone/iPod Touch clients ========================= From 2c9304679b814f51fbb75847eb1c4fa9d26c2ca5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 22 Jan 2011 13:56:01 +0100 Subject: [PATCH 235/235] Add release date for 0.3.0 --- docs/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 68955c3d..cecf3ffa 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,8 +5,8 @@ Changes This change log is used to track all major changes to Mopidy. -0.3.0 (in development) -====================== +0.3.0 (2010-01-22) +================== Mopidy 0.3.0 brings a bunch of small changes all over the place, but no large changes. The main features are support for high bitrate audio from Spotify, and