From 1c620287d48712b76f7d180ef99f5f88f179702e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 27 Dec 2012 03:28:44 +0100 Subject: [PATCH 01/36] Fix racecondition triggered KeyError in our DebugThread. --- mopidy/utils/process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py index 5edf287e..6be8937c 100644 --- a/mopidy/utils/process.py +++ b/mopidy/utils/process.py @@ -101,7 +101,7 @@ class DebugThread(threading.Thread): stack = ''.join(traceback.format_stack(frame)) logger.debug( 'Current state of %s (%s):\n%s', - threads[ident], ident, stack) + threads.get(ident, '?'), ident, stack) del frame self.event.clear() From d9300f72bd6a2e150dea288f122d2bcde22b22a1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Jan 2013 21:39:14 +0100 Subject: [PATCH 02/36] scanner: Support symlinks --- docs/changes.rst | 8 ++++++++ mopidy/utils/path.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 296e7e38..d5245e71 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,6 +5,14 @@ Changes This change log is used to track all major changes to Mopidy. +v0.11.2 (UNRELEASED) +==================== + +**Local backend** + +- Make ``mopidy-scan`` support symlinks. + + v0.11.1 (2012-12-24) ==================== diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index c4fa0ce2..7d988a90 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -120,7 +120,7 @@ def find_files(path): if not os.path.basename(path).startswith(b'.'): yield path else: - for dirpath, dirnames, filenames in os.walk(path): + for dirpath, dirnames, filenames in os.walk(path, followlinks=True): for dirname in dirnames: if dirname.startswith(b'.'): # Skip hidden folders by modifying dirnames inplace From cc69de5626b7258b0bdaf115e32a5b582d5ef2e5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Jan 2013 23:40:59 +0100 Subject: [PATCH 03/36] Fix Python 2.6.0/2.6.1 support (fixes #302) Conflicts: docs/changes.rst --- docs/changes.rst | 5 +++++ mopidy/__main__.py | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index d5245e71..6779ec88 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -8,6 +8,11 @@ This change log is used to track all major changes to Mopidy. v0.11.2 (UNRELEASED) ==================== +(in development) + +- Make Mopidy work on Python 2.6 versions less than 2.6.2rc1. (Fixes: + :issue:`302`) + **Local backend** - Make ``mopidy-scan`` support symlinks. diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 952f158c..e111fcef 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -79,37 +79,40 @@ def main(): def parse_options(): parser = optparse.OptionParser( version='Mopidy %s' % versioning.get_version()) + # NOTE Python 2.6: To support Python versions < 2.6.2rc1 we must use + # bytestrings for the first argument to ``add_option`` + # See https://github.com/mopidy/mopidy/issues/302 for details parser.add_option( - '--help-gst', + b'--help-gst', action='store_true', dest='help_gst', help='show GStreamer help options') parser.add_option( - '-i', '--interactive', + b'-i', '--interactive', action='store_true', dest='interactive', help='ask interactively for required settings which are missing') parser.add_option( - '-q', '--quiet', + b'-q', '--quiet', action='store_const', const=0, dest='verbosity_level', help='less output (warning level)') parser.add_option( - '-v', '--verbose', + b'-v', '--verbose', action='count', default=1, dest='verbosity_level', help='more output (debug level)') parser.add_option( - '--save-debug-log', + b'--save-debug-log', action='store_true', dest='save_debug_log', help='save debug log to "./mopidy.log"') parser.add_option( - '--list-settings', + b'--list-settings', action='callback', callback=settings_utils.list_settings_optparse_callback, help='list current settings') parser.add_option( - '--list-deps', + b'--list-deps', action='callback', callback=deps.list_deps_optparse_callback, help='list dependencies and their versions') parser.add_option( - '--debug-thread', + b'--debug-thread', action='store_true', dest='debug_thread', help='run background thread that dumps tracebacks on SIGUSR1') return parser.parse_args(args=mopidy_args)[0] From 9749f66972c5d9645cf81f5f745a8316faef10db Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 3 Jan 2013 10:04:05 +0100 Subject: [PATCH 04/36] Use bytestrings for the keys of **kwargs dicts Python < 2.6.5rc1 does not work with unicode keys. Fixes #302. --- docs/changes.rst | 9 +++++-- mopidy/backends/local/translator.py | 39 ++++++++++++++++------------- mopidy/scanner.py | 10 +++++--- tests/scanner_test.py | 36 ++++++++++++++------------ 4 files changed, 55 insertions(+), 39 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 6779ec88..ec588ffc 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -10,8 +10,13 @@ v0.11.2 (UNRELEASED) (in development) -- Make Mopidy work on Python 2.6 versions less than 2.6.2rc1. (Fixes: - :issue:`302`) +- Make Mopidy work on early Python 2.6 versions. (Fixes: :issue:`302`) + + - ``optparse`` fails if the first argument to ``add_option`` is a unicode + string on Python < 2.6.2rc1. + + - ``foo(**data)`` fails if the keys in ``data`` is unicode strings on Python + < 2.6.5rc1. **Local backend** diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 390fd92a..157804b4 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -98,6 +98,9 @@ def _convert_mpd_data(data, tracks, music_dir): if not data: return + # NOTE: kwargs are explicitly made bytestrings to work on Python + # 2.6.0/2.6.1. See https://github.com/mopidy/mopidy/issues/302 for details. + track_kwargs = {} album_kwargs = {} artist_kwargs = {} @@ -105,38 +108,38 @@ def _convert_mpd_data(data, tracks, music_dir): if 'track' in data: if '/' in data['track']: - album_kwargs['num_tracks'] = int(data['track'].split('/')[1]) - track_kwargs['track_no'] = int(data['track'].split('/')[0]) + album_kwargs[b'num_tracks'] = int(data['track'].split('/')[1]) + track_kwargs[b'track_no'] = int(data['track'].split('/')[0]) else: - track_kwargs['track_no'] = int(data['track']) + track_kwargs[b'track_no'] = int(data['track']) if 'artist' in data: - artist_kwargs['name'] = data['artist'] - albumartist_kwargs['name'] = data['artist'] + artist_kwargs[b'name'] = data['artist'] + albumartist_kwargs[b'name'] = data['artist'] if 'albumartist' in data: - albumartist_kwargs['name'] = data['albumartist'] + albumartist_kwargs[b'name'] = data['albumartist'] if 'album' in data: - album_kwargs['name'] = data['album'] + album_kwargs[b'name'] = data['album'] if 'title' in data: - track_kwargs['name'] = data['title'] + track_kwargs[b'name'] = data['title'] if 'date' in data: - track_kwargs['date'] = data['date'] + track_kwargs[b'date'] = data['date'] if 'musicbrainz_trackid' in data: - track_kwargs['musicbrainz_id'] = data['musicbrainz_trackid'] + track_kwargs[b'musicbrainz_id'] = data['musicbrainz_trackid'] if 'musicbrainz_albumid' in data: - album_kwargs['musicbrainz_id'] = data['musicbrainz_albumid'] + album_kwargs[b'musicbrainz_id'] = data['musicbrainz_albumid'] if 'musicbrainz_artistid' in data: - artist_kwargs['musicbrainz_id'] = data['musicbrainz_artistid'] + artist_kwargs[b'musicbrainz_id'] = data['musicbrainz_artistid'] if 'musicbrainz_albumartistid' in data: - albumartist_kwargs['musicbrainz_id'] = ( + albumartist_kwargs[b'musicbrainz_id'] = ( data['musicbrainz_albumartistid']) if data['file'][0] == '/': @@ -147,18 +150,18 @@ def _convert_mpd_data(data, tracks, music_dir): if artist_kwargs: artist = Artist(**artist_kwargs) - track_kwargs['artists'] = [artist] + track_kwargs[b'artists'] = [artist] if albumartist_kwargs: albumartist = Artist(**albumartist_kwargs) - album_kwargs['artists'] = [albumartist] + album_kwargs[b'artists'] = [albumartist] if album_kwargs: album = Album(**album_kwargs) - track_kwargs['album'] = album + track_kwargs[b'album'] = album - track_kwargs['uri'] = path_to_uri(music_dir, path) - track_kwargs['length'] = int(data.get('time', 0)) * 1000 + track_kwargs[b'uri'] = path_to_uri(music_dir, path) + track_kwargs[b'length'] = int(data.get('time', 0)) * 1000 track = Track(**track_kwargs) tracks.add(track) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 0b10d061..68d7440a 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -96,9 +96,13 @@ def translator(data): artist_kwargs = {} track_kwargs = {} + # NOTE: kwargs are explicitly made bytestrings to work on Python + # 2.6.0/2.6.1. See https://github.com/mopidy/mopidy/issues/302 for + # details. + def _retrieve(source_key, target_key, target): if source_key in data: - target[target_key] = data[source_key] + target[str(target_key)] = data[source_key] _retrieve(gst.TAG_ALBUM, 'name', album_kwargs) _retrieve(gst.TAG_TRACK_COUNT, 'num_tracks', album_kwargs) @@ -111,7 +115,7 @@ def translator(data): except ValueError: pass # Ignore invalid dates else: - track_kwargs['date'] = date.isoformat() + track_kwargs[b'date'] = date.isoformat() _retrieve(gst.TAG_TITLE, 'name', track_kwargs) _retrieve(gst.TAG_TRACK_NUMBER, 'track_no', track_kwargs) @@ -125,7 +129,7 @@ def translator(data): 'musicbrainz-albumartistid', 'musicbrainz_id', albumartist_kwargs) if albumartist_kwargs: - album_kwargs['artists'] = [Artist(**albumartist_kwargs)] + album_kwargs[b'artists'] = [Artist(**albumartist_kwargs)] track_kwargs['uri'] = data['uri'] track_kwargs['length'] = data[gst.TAG_DURATION] diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 92e9a269..d8466e26 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -32,36 +32,40 @@ class TranslatorTest(unittest.TestCase): 'musicbrainz-albumartistid': 'mbalbumartistid', } + # NOTE: kwargs are explicitly made bytestrings to work on Python + # 2.6.0/2.6.1. See https://github.com/mopidy/mopidy/issues/302 for + # details. + self.album = { - 'name': 'albumname', - 'num_tracks': 2, - 'musicbrainz_id': 'mbalbumid', + b'name': 'albumname', + b'num_tracks': 2, + b'musicbrainz_id': 'mbalbumid', } self.artist = { - 'name': 'name', - 'musicbrainz_id': 'mbartistid', + b'name': 'name', + b'musicbrainz_id': 'mbartistid', } self.albumartist = { - 'name': 'albumartistname', - 'musicbrainz_id': 'mbalbumartistid', + b'name': 'albumartistname', + b'musicbrainz_id': 'mbalbumartistid', } self.track = { - 'uri': 'uri', - 'name': 'trackname', - 'date': '2006-01-01', - 'track_no': 1, - 'length': 4531, - 'musicbrainz_id': 'mbtrackid', + b'uri': 'uri', + b'name': 'trackname', + b'date': '2006-01-01', + b'track_no': 1, + b'length': 4531, + b'musicbrainz_id': 'mbtrackid', } 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)] + self.album[b'artists'] = [Artist(**self.albumartist)] + self.track[b'album'] = Album(**self.album) + self.track[b'artists'] = [Artist(**self.artist)] return Track(**self.track) def check(self): From aebadc9b753a32322fc6b2497c2deae1386c2558 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 3 Jan 2013 12:58:08 +0100 Subject: [PATCH 05/36] More **kwargs key fixing (#302) --- mopidy/scanner.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 68d7440a..aba2491c 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -131,10 +131,10 @@ def translator(data): if albumartist_kwargs: album_kwargs[b'artists'] = [Artist(**albumartist_kwargs)] - track_kwargs['uri'] = data['uri'] - track_kwargs['length'] = data[gst.TAG_DURATION] - track_kwargs['album'] = Album(**album_kwargs) - track_kwargs['artists'] = [Artist(**artist_kwargs)] + track_kwargs[b'uri'] = data['uri'] + track_kwargs[b'length'] = data[gst.TAG_DURATION] + track_kwargs[b'album'] = Album(**album_kwargs) + track_kwargs[b'artists'] = [Artist(**artist_kwargs)] return Track(**track_kwargs) From 5d1e01762bcb7ce423eafb38552dce9639f1d5d3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 3 Jan 2013 13:14:21 +0100 Subject: [PATCH 06/36] scanner: Fix optparse on early 2.6 (#302) --- mopidy/scanner.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index aba2491c..9f8c12f7 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -79,12 +79,15 @@ def main(): def parse_options(): parser = optparse.OptionParser( version='Mopidy %s' % versioning.get_version()) + # NOTE Python 2.6: To support Python versions < 2.6.2rc1 we must use + # bytestrings for the first argument to ``add_option`` + # See https://github.com/mopidy/mopidy/issues/302 for details parser.add_option( - '-q', '--quiet', + b'-q', '--quiet', action='store_const', const=0, dest='verbosity_level', help='less output (warning level)') parser.add_option( - '-v', '--verbose', + b'-v', '--verbose', action='count', default=1, dest='verbosity_level', help='more output (debug level)') return parser.parse_args(args=mopidy_args)[0] From b3a7ed3e53d0da6749b86e93b8706f50d41f19c9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 3 Jan 2013 21:37:31 +0100 Subject: [PATCH 07/36] js: Add a package.json to simplify JS dev env setup --- js/README.rst | 39 +++++++++++++-------------------------- js/grunt.js | 4 +++- js/package.json | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 27 deletions(-) create mode 100644 js/package.json diff --git a/js/README.rst b/js/README.rst index a68dd9a0..e8782213 100644 --- a/js/README.rst +++ b/js/README.rst @@ -36,40 +36,27 @@ Building from source sudo apt-get update sudo apt-get install nodejs npm -2. Assuming you install from PPA, setup your ``NODE_PATH`` environment variable - to include ``/usr/lib/node_modules``. Add the following to your - ``~/.bashrc`` or equivalent:: - - export NODE_PATH=/usr/lib/node_modules:$NODE_PATH - -3. Install `Buster.js `_ and `Grunt - `_ globally (or locally, and make sure you get their - binaries on your ``PATH``):: - - sudo npm -g install buster grunt - -4. Install the grunt-buster Grunt plugin locally, when in the ``js/`` dir:: +2. Enter the ``js/`` dir and install development dependencies:: cd js/ - npm install grunt-buster + npm install -5. Install `PhantomJS `_ so that we can run the tests - without a browser:: +That's it. - sudo apt-get install phantomjs +You can now run the tests:: - It is packaged in Ubuntu since 12.04, but I haven't tested with versions - older than 1.6 which is the one packaged in Ubuntu 12.10. + npm test -6. Run Grunt to lint, test, concatenate, and minify the source:: +To run tests automatically when you save a file:: - grunt + npm run-script watch - The files in ``../mopidy/frontends/http/data/`` should now be up to date. +To run tests, concatenate, minify the source, and update the JavaScript files +in ``mopidy/frontends/http/data/``:: + npm run-script build -Development tips -================ +To run other `grunt `_ targets which isn't predefined in +``package.json`` and thus isn't available through ``npm run-script``:: -If you're coding on the JavaScript library, you should know about ``grunt -watch``. It lints and tests the code every time you save a file. + PATH=./node_modules/.bin:$PATH grunt foo diff --git a/js/grunt.js b/js/grunt.js index d835fd77..46afc8af 100644 --- a/js/grunt.js +++ b/js/grunt.js @@ -64,7 +64,9 @@ module.exports = function (grunt) { uglify: {} }); - grunt.registerTask("default", "lint buster concat min"); + grunt.registerTask("test", "lint buster"); + grunt.registerTask("build", "lint buster concat min"); + grunt.registerTask("default", "build"); grunt.loadNpmTasks("grunt-buster"); }; diff --git a/js/package.json b/js/package.json new file mode 100644 index 00000000..a8737cfb --- /dev/null +++ b/js/package.json @@ -0,0 +1,15 @@ +{ + "name": "mopidy", + "version": "0.0.0", + "devDependencies": { + "buster": "*", + "grunt": "*", + "grunt-buster": "*", + "phantomjs": "*" + }, + "scripts": { + "test": "grunt test", + "build": "grunt build", + "watch": "grunt watch" + } +} From f387d354632534b0b484da3dcd51498b59a46b1b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 3 Jan 2013 22:24:58 +0100 Subject: [PATCH 08/36] audio: Update mixer track selection logic (fixes #307) We now ensure that the track we choose has one or more volume channels we can control. This change also fixes that fact the MIXER_TRACK setting would not work if we happened to find a track that was flaged as MASTER OUPUT before finding the right label, so far no one has reported this as an issue. --- mopidy/audio/actor.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 1b6c79b3..f6f8dbe9 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -158,15 +158,23 @@ class Audio(pykka.ThreadingActor): mixer.get_factory().get_name(), track.label) def _select_mixer_track(self, mixer, track_label): - # Look for track with label == MIXER_TRACK, otherwise fallback to - # master track which is also an output. + # Ignore tracks without volumes, then look for track with + # label == settings.MIXER_TRACK, otherwise fallback to first usable + # track hoping the mixer gave them to us in a sensible order. + + usable_tracks = [] for track in mixer.list_tracks(): - if track_label: - if track.label == track_label: - return track + if not mixer.get_volume(track): + continue + + if track_label and track.label == track_label: + return track elif track.flags & (gst.interfaces.MIXER_TRACK_MASTER | gst.interfaces.MIXER_TRACK_OUTPUT): - return track + usable_tracks.append(track) + + if usable_tracks: + return usable_tracks[0] def _teardown_mixer(self): if self._mixer is not None: From 44d1124574facec8534c8f83218172b8933776d4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 3 Jan 2013 22:45:30 +0100 Subject: [PATCH 09/36] mpd: Use bytestring for **kwargs key (#302) --- mopidy/frontends/mpd/actor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index 11e07aa7..33ccd077 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -19,10 +19,12 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME) port = settings.MPD_SERVER_PORT + # NOTE: dict key must be bytestring to work on Python < 2.6.5 + # See https://github.com/mopidy/mopidy/issues/302 for details try: network.Server( hostname, port, - protocol=session.MpdSession, protocol_kwargs={'core': core}, + protocol=session.MpdSession, protocol_kwargs={b'core': core}, max_connections=settings.MPD_SERVER_MAX_CONNECTIONS, timeout=settings.MPD_SERVER_CONNECTION_TIMEOUT) except IOError as error: From 0404ec625b2b3e82706a0c75fc3d36856f5cbd2a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 3 Jan 2013 22:54:13 +0100 Subject: [PATCH 10/36] Update changelog with mixer track selection change. Conflicts: docs/changes.rst --- docs/changes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index ec588ffc..1f3d8484 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -18,6 +18,8 @@ v0.11.2 (UNRELEASED) - ``foo(**data)`` fails if the keys in ``data`` is unicode strings on Python < 2.6.5rc1. +- Improve selection of mixer tracks for volume control. (Fixes: :issuse:`307`) + **Local backend** - Make ``mopidy-scan`` support symlinks. From 3ab6748c339075aa1ce5f4685a31c46c7ed73884 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Jan 2013 10:26:30 +0100 Subject: [PATCH 11/36] mpd: Make request handler **kwargs keys bytestrings (#302) --- mopidy/frontends/mpd/protocol/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py index 1827624b..55a1563b 100644 --- a/mopidy/frontends/mpd/protocol/__init__.py +++ b/mopidy/frontends/mpd/protocol/__init__.py @@ -56,7 +56,12 @@ def handle_request(pattern, auth_required=True): if match is not None: mpd_commands.add( MpdCommand(name=match.group(), auth_required=auth_required)) - compiled_pattern = re.compile(pattern, flags=re.UNICODE) + # NOTE: Make pattern a bytestring to get bytestring keys in the dict + # returned from matches.groupdict(), which is again used as a **kwargs + # dict. This is needed to work on Python < 2.6.5. See + # https://github.com/mopidy/mopidy/issues/302 for details. + bytestring_pattern = pattern.encode('utf-8') + compiled_pattern = re.compile(bytestring_pattern, flags=re.UNICODE) if compiled_pattern in request_handlers: raise ValueError('Tried to redefine handler for %s with %s' % ( pattern, func)) From 5c6a2e02ba0079c43e8d8e129e60a1b80582c465 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 6 Jan 2013 20:48:38 +0100 Subject: [PATCH 12/36] Fix more unicode kwargs dict keys (#302) --- mopidy/__main__.py | 3 +-- mopidy/backends/local/translator.py | 4 +-- mopidy/frontends/mpd/actor.py | 4 +-- mopidy/frontends/mpd/protocol/__init__.py | 6 ++--- mopidy/frontends/mpd/translator.py | 7 ++--- mopidy/models.py | 32 ++++++++++++++++------- mopidy/scanner.py | 8 +++--- 7 files changed, 37 insertions(+), 27 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index e111fcef..2847497a 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -79,8 +79,7 @@ def main(): def parse_options(): parser = optparse.OptionParser( version='Mopidy %s' % versioning.get_version()) - # NOTE Python 2.6: To support Python versions < 2.6.2rc1 we must use - # bytestrings for the first argument to ``add_option`` + # NOTE First argument to add_option must be bytestrings on Python < 2.6.2 # See https://github.com/mopidy/mopidy/issues/302 for details parser.add_option( b'--help-gst', diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 157804b4..b029d367 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -98,8 +98,8 @@ def _convert_mpd_data(data, tracks, music_dir): if not data: return - # NOTE: kwargs are explicitly made bytestrings to work on Python - # 2.6.0/2.6.1. See https://github.com/mopidy/mopidy/issues/302 for details. + # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 + # See https://github.com/mopidy/mopidy/issues/302 for details. track_kwargs = {} album_kwargs = {} diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index 33ccd077..8907fe22 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -19,8 +19,8 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME) port = settings.MPD_SERVER_PORT - # NOTE: dict key must be bytestring to work on Python < 2.6.5 - # See https://github.com/mopidy/mopidy/issues/302 for details + # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 + # See https://github.com/mopidy/mopidy/issues/302 for details. try: network.Server( hostname, port, diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py index 55a1563b..2b2260ef 100644 --- a/mopidy/frontends/mpd/protocol/__init__.py +++ b/mopidy/frontends/mpd/protocol/__init__.py @@ -56,10 +56,10 @@ def handle_request(pattern, auth_required=True): if match is not None: mpd_commands.add( MpdCommand(name=match.group(), auth_required=auth_required)) - # NOTE: Make pattern a bytestring to get bytestring keys in the dict + # NOTE Make pattern a bytestring to get bytestring keys in the dict # returned from matches.groupdict(), which is again used as a **kwargs - # dict. This is needed to work on Python < 2.6.5. See - # https://github.com/mopidy/mopidy/issues/302 for details. + # dict. This is needed to work on Python < 2.6.5. + # See https://github.com/mopidy/mopidy/issues/302 for details. bytestring_pattern = pattern.encode('utf-8') compiled_pattern = re.compile(bytestring_pattern, flags=re.UNICODE) if compiled_pattern in request_handlers: diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index e26d7dce..15ca181d 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -140,6 +140,8 @@ def query_from_mpd_list_format(field, mpd_query): """ Converts an MPD ``list`` query to a Mopidy query. """ + # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 + # See https://github.com/mopidy/mopidy/issues/302 for details if mpd_query is None: return {} try: @@ -155,15 +157,14 @@ def query_from_mpd_list_format(field, mpd_query): if field == 'album': if not tokens[0]: raise ValueError - return {'artist': [tokens[0]]} + return {b'artist': [tokens[0]]} # See above NOTE else: raise MpdArgError( 'should be "Album" for 3 arguments', command='list') elif len(tokens) % 2 == 0: query = {} while tokens: - key = tokens[0].lower() - key = str(key) # Needed for kwargs keys on OS X and Windows + key = str(tokens[0].lower()) # See above NOTE value = tokens[1] tokens = tokens[2:] if key not in ('artist', 'album', 'date', 'genre'): diff --git a/mopidy/models.py b/mopidy/models.py index 73209b6e..1fb3d7a7 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -66,13 +66,15 @@ class ImmutableObject(object): :type values: dict :rtype: new instance of the model being copied """ + # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 + # See https://github.com/mopidy/mopidy/issues/302 for details data = {} for key in self.__dict__.keys(): public_key = key.lstrip('_') - data[public_key] = values.pop(public_key, self.__dict__[key]) + data[str(public_key)] = values.pop(public_key, self.__dict__[key]) for key in values.keys(): if hasattr(self, key): - data[key] = values.pop(key) + data[str(key)] = values.pop(key) if values: raise TypeError( 'copy() got an unexpected keyword argument "%s"' % key) @@ -186,7 +188,9 @@ class Album(ImmutableObject): musicbrainz_id = None def __init__(self, *args, **kwargs): - self.__dict__['artists'] = frozenset(kwargs.pop('artists', [])) + # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 + # See https://github.com/mopidy/mopidy/issues/302 for details + self.__dict__[b'artists'] = frozenset(kwargs.pop('artists', [])) super(Album, self).__init__(*args, **kwargs) @@ -240,7 +244,9 @@ class Track(ImmutableObject): musicbrainz_id = None def __init__(self, *args, **kwargs): - self.__dict__['artists'] = frozenset(kwargs.pop('artists', [])) + # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 + # See https://github.com/mopidy/mopidy/issues/302 for details + self.__dict__[b'artists'] = frozenset(kwargs.pop('artists', [])) super(Track, self).__init__(*args, **kwargs) @@ -272,9 +278,11 @@ class TlTrack(ImmutableObject): track = None def __init__(self, *args, **kwargs): + # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 + # See https://github.com/mopidy/mopidy/issues/302 for details if len(args) == 2 and len(kwargs) == 0: - kwargs['tlid'] = args[0] - kwargs['track'] = args[1] + kwargs[b'tlid'] = args[0] + kwargs[b'track'] = args[1] args = [] super(TlTrack, self).__init__(*args, **kwargs) @@ -309,7 +317,9 @@ class Playlist(ImmutableObject): last_modified = None def __init__(self, *args, **kwargs): - self.__dict__['tracks'] = tuple(kwargs.pop('tracks', [])) + # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 + # See https://github.com/mopidy/mopidy/issues/302 for details + self.__dict__[b'tracks'] = tuple(kwargs.pop('tracks', [])) super(Playlist, self).__init__(*args, **kwargs) # TODO: def insert(self, pos, track): ... ? @@ -345,7 +355,9 @@ class SearchResult(ImmutableObject): albums = tuple() def __init__(self, *args, **kwargs): - self.__dict__['tracks'] = tuple(kwargs.pop('tracks', [])) - self.__dict__['artists'] = tuple(kwargs.pop('artists', [])) - self.__dict__['albums'] = tuple(kwargs.pop('albums', [])) + # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 + # See https://github.com/mopidy/mopidy/issues/302 for details + self.__dict__[b'tracks'] = tuple(kwargs.pop('tracks', [])) + self.__dict__[b'artists'] = tuple(kwargs.pop('artists', [])) + self.__dict__[b'albums'] = tuple(kwargs.pop('albums', [])) super(SearchResult, self).__init__(*args, **kwargs) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 9f8c12f7..bfeb9fd1 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -79,8 +79,7 @@ def main(): def parse_options(): parser = optparse.OptionParser( version='Mopidy %s' % versioning.get_version()) - # NOTE Python 2.6: To support Python versions < 2.6.2rc1 we must use - # bytestrings for the first argument to ``add_option`` + # NOTE First argument to add_option must be bytestrings on Python < 2.6.2 # See https://github.com/mopidy/mopidy/issues/302 for details parser.add_option( b'-q', '--quiet', @@ -99,9 +98,8 @@ def translator(data): artist_kwargs = {} track_kwargs = {} - # NOTE: kwargs are explicitly made bytestrings to work on Python - # 2.6.0/2.6.1. See https://github.com/mopidy/mopidy/issues/302 for - # details. + # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 + # See https://github.com/mopidy/mopidy/issues/302 for details. def _retrieve(source_key, target_key, target): if source_key in data: From 04b24b4a37d78060022e468e6d5b8155d4073440 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 6 Jan 2013 21:04:27 +0100 Subject: [PATCH 13/36] core: Add 'include_tracks' argument to 'get_playlists()' Conflicts: docs/changes.rst --- docs/changes.rst | 9 +++++++++ mopidy/core/playlists.py | 7 +++++-- tests/core/playlists_test.py | 16 ++++++++++++++++ tests/utils/jsonrpc_test.py | 2 +- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 1f3d8484..f1df2ae4 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -24,6 +24,15 @@ v0.11.2 (UNRELEASED) - Make ``mopidy-scan`` support symlinks. +**Core API** + +- :meth:`mopidy.core.PlaylistsController.get_playlists` now accepts an argument + ``include_tracks``. This defaults to :class:`True`, which has the same old + behavior. If set to :class:`False`, the tracks are stripped from the + playlists before they are returned. This can be used to limit the amount of + data returned if the response is to be passed out of the application, e.g. to + a web client. (Fixes: :issue:`297`) + v0.11.1 (2012-12-24) ==================== diff --git a/mopidy/core/playlists.py b/mopidy/core/playlists.py index 62098c7f..f0187d44 100644 --- a/mopidy/core/playlists.py +++ b/mopidy/core/playlists.py @@ -15,11 +15,14 @@ class PlaylistsController(object): self.backends = backends self.core = core - def get_playlists(self): + def get_playlists(self, include_tracks=True): futures = [ b.playlists.playlists for b in self.backends.with_playlists] results = pykka.get_all(futures) - return list(itertools.chain(*results)) + playlists = list(itertools.chain(*results)) + if not include_tracks: + playlists = [p.copy(tracks=[]) for p in playlists] + return playlists playlists = property(get_playlists) """ diff --git a/tests/core/playlists_test.py b/tests/core/playlists_test.py index cea93c5b..f11e1776 100644 --- a/tests/core/playlists_test.py +++ b/tests/core/playlists_test.py @@ -46,6 +46,22 @@ class PlaylistsTest(unittest.TestCase): self.assertIn(self.pl2a, result) self.assertIn(self.pl2b, result) + def test_get_playlists_includes_tracks_by_default(self): + result = self.core.playlists.get_playlists() + + self.assertEqual(result[0].name, 'A') + self.assertEqual(len(result[0].tracks), 1) + self.assertEqual(result[1].name, 'B') + self.assertEqual(len(result[1].tracks), 1) + + def test_get_playlist_can_strip_tracks_from_returned_playlists(self): + result = self.core.playlists.get_playlists(include_tracks=False) + + self.assertEqual(result[0].name, 'A') + self.assertEqual(len(result[0].tracks), 0) + self.assertEqual(result[1].name, 'B') + self.assertEqual(len(result[1].tracks), 0) + def test_create_without_uri_scheme_uses_first_backend(self): playlist = Playlist() self.sp1.create().get.return_value = playlist diff --git a/tests/utils/jsonrpc_test.py b/tests/utils/jsonrpc_test.py index 59cb89b5..db53d4e4 100644 --- a/tests/utils/jsonrpc_test.py +++ b/tests/utils/jsonrpc_test.py @@ -603,7 +603,7 @@ class JsonRpcInspectorTest(JsonRpcTestBase): self.assertIn('core.playlists.get_playlists', methods) self.assertEquals( - len(methods['core.playlists.get_playlists']['params']), 0) + len(methods['core.playlists.get_playlists']['params']), 1) self.assertIn('core.tracklist.filter', methods.keys()) self.assertEquals( From 21e792acef4bcb1ade31114f6045195727ee1b8c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 6 Jan 2013 21:33:01 +0100 Subject: [PATCH 14/36] Don't exit autotest if a test fails --- fabfile.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fabfile.py b/fabfile.py index 370c81be..3321cb16 100644 --- a/fabfile.py +++ b/fabfile.py @@ -1,4 +1,4 @@ -from fabric.api import local +from fabric.api import local, settings def test(path=None): @@ -9,7 +9,8 @@ def test(path=None): def autotest(path=None): while True: local('clear') - test(path) + with settings(warn_only=True): + test(path) local( 'inotifywait -q -e create -e modify -e delete ' '--exclude ".*\.(pyc|sw.)" -r mopidy/ tests/') From bb9ef63a428b5b0cdd82608664976edbb326b4f7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 6 Jan 2013 21:43:12 +0100 Subject: [PATCH 15/36] models: Add Album.images field (#263) --- docs/changes.rst | 5 +++++ mopidy/models.py | 6 ++++++ tests/models_test.py | 11 +++++++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index f1df2ae4..82ec44b5 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -33,6 +33,11 @@ v0.11.2 (UNRELEASED) data returned if the response is to be passed out of the application, e.g. to a web client. (Fixes: :issue:`297`) +**Models** + +- Add :attr:`mopidy.models.Album.images` field for including album art URIs. + (Partly fixes :issue:`263`) + v0.11.1 (2012-12-24) ==================== diff --git a/mopidy/models.py b/mopidy/models.py index 1fb3d7a7..1a165222 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -167,6 +167,8 @@ class Album(ImmutableObject): :type date: string :param musicbrainz_id: MusicBrainz ID :type musicbrainz_id: string + :param images: album image URIs + :type images: list of strings """ #: The album URI. Read-only. @@ -187,10 +189,14 @@ class Album(ImmutableObject): #: The MusicBrainz ID of the album. Read-only. musicbrainz_id = None + #: The album image URIs. Read-only. + images = frozenset() + def __init__(self, *args, **kwargs): # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 # See https://github.com/mopidy/mopidy/issues/302 for details self.__dict__[b'artists'] = frozenset(kwargs.pop('artists', [])) + self.__dict__[b'images'] = frozenset(kwargs.pop('images', [])) super(Album, self).__init__(*args, **kwargs) diff --git a/tests/models_test.py b/tests/models_test.py index 89d0b132..ef3fd68c 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -216,18 +216,25 @@ class AlbumTest(unittest.TestCase): self.assertRaises( AttributeError, setattr, album, 'musicbrainz_id', None) + def test_images(self): + image = 'data:foobar' + album = Album(images=[image]) + self.assertIn(image, album.images) + self.assertRaises(AttributeError, setattr, album, 'images', None) + def test_invalid_kwarg(self): test = lambda: Album(foo='baz') self.assertRaises(TypeError, test) def test_repr_without_artists(self): self.assertEquals( - "Album(artists=[], name=u'name', uri=u'uri')", + "Album(artists=[], images=[], name=u'name', uri=u'uri')", repr(Album(uri='uri', name='name'))) def test_repr_with_artists(self): self.assertEquals( - "Album(artists=[Artist(name=u'foo')], name=u'name', uri=u'uri')", + "Album(artists=[Artist(name=u'foo')], images=[], name=u'name', " + "uri=u'uri')", repr(Album(uri='uri', name='name', artists=[Artist(name='foo')]))) def test_serialize_without_artists(self): From f3ccf871e5aa04430477701ba1f55dcb7af9706d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 6 Jan 2013 22:02:26 +0100 Subject: [PATCH 16/36] models: Add Track.disc_no (#286) --- docs/changes.rst | 2 ++ mopidy/models.py | 7 ++++++- tests/models_test.py | 6 ++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 82ec44b5..475d30db 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -38,6 +38,8 @@ v0.11.2 (UNRELEASED) - Add :attr:`mopidy.models.Album.images` field for including album art URIs. (Partly fixes :issue:`263`) +- Add :attr:`mopidy.models.Track.disc_no` field. (Partly fixes: :issue:`286`) + v0.11.1 (2012-12-24) ==================== diff --git a/mopidy/models.py b/mopidy/models.py index 1a165222..1e9ad2c1 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -212,6 +212,8 @@ class Track(ImmutableObject): :type album: :class:`Album` :param track_no: track number in album :type track_no: integer + :param disc_no: disc number in album + :type disc_no: integer or :class:`None` if unknown :param date: track release date (YYYY or YYYY-MM-DD) :type date: string :param length: track length in milliseconds @@ -234,9 +236,12 @@ class Track(ImmutableObject): #: The track :class:`Album`. Read-only. album = None - #: The track number in album. Read-only. + #: The track number in the album. Read-only. track_no = 0 + #: The disc number in the album. Read-only. + disc_no = None + #: The track release date. Read-only. date = None diff --git a/tests/models_test.py b/tests/models_test.py index ef3fd68c..fdac575b 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -396,6 +396,12 @@ class TrackTest(unittest.TestCase): self.assertEqual(track.track_no, track_no) self.assertRaises(AttributeError, setattr, track, 'track_no', None) + def test_disc_no(self): + disc_no = 2 + track = Track(disc_no=disc_no) + self.assertEqual(track.disc_no, disc_no) + self.assertRaises(AttributeError, setattr, track, 'disc_no', None) + def test_date(self): date = '1977-01-01' track = Track(date=date) From d2a51f712868372d51d30881437b0628fca35fa6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 6 Jan 2013 22:04:56 +0100 Subject: [PATCH 17/36] models: Add Album.num_discs (#286) --- docs/changes.rst | 2 ++ mopidy/models.py | 5 +++++ tests/models_test.py | 8 +++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 475d30db..2f424cd7 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -40,6 +40,8 @@ v0.11.2 (UNRELEASED) - Add :attr:`mopidy.models.Track.disc_no` field. (Partly fixes: :issue:`286`) +- Add :attr:`mopidy.models.Album.num_discs` field. (Partly fixes: :issue:`286`) + v0.11.1 (2012-12-24) ==================== diff --git a/mopidy/models.py b/mopidy/models.py index 1e9ad2c1..39d29fd3 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -163,6 +163,8 @@ class Album(ImmutableObject): :type artists: list of :class:`Artist` :param num_tracks: number of tracks in album :type num_tracks: integer + :param num_discs: number of discs in album + :type num_discs: integer or :class:`None` if unknown :param date: album release date (YYYY or YYYY-MM-DD) :type date: string :param musicbrainz_id: MusicBrainz ID @@ -183,6 +185,9 @@ class Album(ImmutableObject): #: The number of tracks in the album. Read-only. num_tracks = 0 + #: The number of discs in the album. Read-only. + num_discs = None + #: The album release date. Read-only. date = None diff --git a/tests/models_test.py b/tests/models_test.py index fdac575b..08a8c6f0 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -199,10 +199,16 @@ class AlbumTest(unittest.TestCase): def test_num_tracks(self): num_tracks = 11 - album = Album(num_tracks=11) + album = Album(num_tracks=num_tracks) self.assertEqual(album.num_tracks, num_tracks) self.assertRaises(AttributeError, setattr, album, 'num_tracks', None) + def test_num_discs(self): + num_discs = 2 + album = Album(num_discs=num_discs) + self.assertEqual(album.num_discs, num_discs) + self.assertRaises(AttributeError, setattr, album, 'num_discs', None) + def test_date(self): date = '1977-01-01' album = Album(date=date) From e0f220921e6b5256c23c59f074159b0f2ca38a7f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 6 Jan 2013 22:37:37 +0100 Subject: [PATCH 18/36] models: Support serialization of lists of strings --- mopidy/models.py | 4 +++- tests/models_test.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/mopidy/models.py b/mopidy/models.py index 39d29fd3..b1f0b351 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -87,7 +87,9 @@ class ImmutableObject(object): public_key = key.lstrip('_') value = self.__dict__[key] if isinstance(value, (set, frozenset, list, tuple)): - value = [o.serialize() for o in value] + value = [ + v.serialize() if isinstance(v, ImmutableObject) else v + for v in value] elif isinstance(value, ImmutableObject): value = value.serialize() if value: diff --git a/tests/models_test.py b/tests/models_test.py index 08a8c6f0..c2d65ec6 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -255,6 +255,13 @@ class AlbumTest(unittest.TestCase): 'artists': [artist.serialize()]}, Album(uri='uri', name='name', artists=[artist]).serialize()) + def test_serialize_with_images(self): + image = 'data:foobar' + self.assertDictEqual( + {'__model__': 'Album', 'uri': 'uri', 'name': 'name', + 'images': [image]}, + Album(uri='uri', name='name', images=[image]).serialize()) + def test_to_json_and_back(self): album1 = Album(uri='uri', name='name', artists=[Artist(name='foo')]) serialized = json.dumps(album1, cls=ModelJSONEncoder) From b1e58f7079bed005201317ae8210d1d591c9b066 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 6 Jan 2013 23:48:53 +0100 Subject: [PATCH 19/36] Fix more unicode kwargs dict keys (#302) --- mopidy/models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mopidy/models.py b/mopidy/models.py index b1f0b351..e14fd8b4 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -127,11 +127,16 @@ def model_json_decoder(dct): {u'a_track': Track(artists=[], name=u'name')} """ + # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 + # See https://github.com/mopidy/mopidy/issues/302 for details. if '__model__' in dct: model_name = dct.pop('__model__') cls = globals().get(model_name, None) if issubclass(cls, ImmutableObject): - return cls(**dct) + kwargs = {} + for key, value in dct.items(): + kwargs[str(key)] = value + return cls(**kwargs) return dct From a36c94fcbab62f058e6125b76b7be2192f4d39b9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Jan 2013 21:50:40 +0100 Subject: [PATCH 20/36] Fix one more kwargs dict (#302) --- tests/utils/jsonrpc_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/utils/jsonrpc_test.py b/tests/utils/jsonrpc_test.py index db53d4e4..44ec1b09 100644 --- a/tests/utils/jsonrpc_test.py +++ b/tests/utils/jsonrpc_test.py @@ -201,10 +201,12 @@ class JsonRpcSingleCommandTest(JsonRpcTestBase): self.assertEqual(self.core.playback.get_volume().get(), 37) def test_call_methods_with_named_params(self): + # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 + # See https://github.com/mopidy/mopidy/issues/302 for details. request = { 'jsonrpc': '2.0', 'method': 'core.playback.set_volume', - 'params': {'volume': 37}, + 'params': {b'volume': 37}, 'id': 1, } response = self.jrw.handle_data(request) From d3e41dc5e2a1bfe1e96205c72e12af9b84c42521 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Jan 2013 21:51:11 +0100 Subject: [PATCH 21/36] tests: Add better error message to debug test failure --- tests/utils/encoding_test.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/utils/encoding_test.py b/tests/utils/encoding_test.py index 1a4e56c5..b7292de0 100644 --- a/tests/utils/encoding_test.py +++ b/tests/utils/encoding_test.py @@ -15,15 +15,19 @@ class LocaleDecodeTest(unittest.TestCase): result = locale_decode( b'[Errno 98] Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e') - self.assertEquals('[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result) + self.assertEqual('[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result) def test_can_decode_an_ioerror_with_french_content(self, mock): mock.return_value = 'UTF-8' error = IOError(98, b'Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e') result = locale_decode(error) + expected = '[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e' - self.assertEquals('[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result) + self.assertEqual( + expected, result, + '%r decoded to %r does not match expected %r' % ( + error, result, expected)) def test_does_not_use_locale_to_decode_unicode_strings(self, mock): mock.return_value = 'UTF-8' From 708372993048f6b8e261ff7de01153b07d29ee66 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Jan 2013 00:05:27 +0100 Subject: [PATCH 22/36] docs: Fix typos in changelog Conflicts: docs/changes.rst --- docs/changes.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 2f424cd7..730cf275 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -18,7 +18,9 @@ v0.11.2 (UNRELEASED) - ``foo(**data)`` fails if the keys in ``data`` is unicode strings on Python < 2.6.5rc1. -- Improve selection of mixer tracks for volume control. (Fixes: :issuse:`307`) +**Audio sub-system** + +- Improve selection of mixer tracks for volume control. (Fixes: :issue:`307`) **Local backend** From f675a7e01d292c7aa6f79e33ae697c5c23ef049b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 16 Jan 2013 20:38:52 +0100 Subject: [PATCH 23/36] js: Make 'Mopidy()' work without 'new' --- js/src/mopidy.js | 4 ++++ js/test/mopidy-test.js | 9 +++++++++ mopidy/frontends/http/data/mopidy.js | 8 ++++++-- mopidy/frontends/http/data/mopidy.min.js | 6 +++--- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/js/src/mopidy.js b/js/src/mopidy.js index 3485b442..5a75a836 100644 --- a/js/src/mopidy.js +++ b/js/src/mopidy.js @@ -1,6 +1,10 @@ /*global bane:false, when:false*/ function Mopidy(settings) { + if (!(this instanceof Mopidy)) { + return new Mopidy(settings); + } + this._settings = this._configure(settings || {}); this._console = this._getConsole(); diff --git a/js/test/mopidy-test.js b/js/test/mopidy-test.js index 7dcc5972..8842ebf4 100644 --- a/js/test/mopidy-test.js +++ b/js/test/mopidy-test.js @@ -48,6 +48,15 @@ buster.testCase("Mopidy", { new Mopidy({webSocket: {}}); refute.called(this.webSocketConstructorStub); + }, + + "works without 'new' keyword": function () { + var mopidyConstructor = Mopidy; // To trick jshint into submission + + var mopidy = mopidyConstructor({webSocket: {}}); + + assert.isObject(mopidy); + assert(mopidy instanceof Mopidy); } }, diff --git a/mopidy/frontends/http/data/mopidy.js b/mopidy/frontends/http/data/mopidy.js index 5b022c0c..f41695e6 100644 --- a/mopidy/frontends/http/data/mopidy.js +++ b/mopidy/frontends/http/data/mopidy.js @@ -1,6 +1,6 @@ -/*! Mopidy.js - built 2012-12-04 +/*! Mopidy.js - built 2013-01-16 * http://www.mopidy.com/ - * Copyright (c) 2012 Stein Magnus Jodal and contributors + * Copyright (c) 2013 Stein Magnus Jodal and contributors * Licensed under the Apache License, Version 2.0 */ /** @@ -910,6 +910,10 @@ define(['module'], function () { /*global bane:false, when:false*/ function Mopidy(settings) { + if (!(this instanceof Mopidy)) { + return new Mopidy(settings); + } + this._settings = this._configure(settings || {}); this._console = this._getConsole(); diff --git a/mopidy/frontends/http/data/mopidy.min.js b/mopidy/frontends/http/data/mopidy.min.js index 797b47ec..e727cefd 100644 --- a/mopidy/frontends/http/data/mopidy.min.js +++ b/mopidy/frontends/http/data/mopidy.min.js @@ -1,5 +1,5 @@ -/*! Mopidy.js - built 2012-12-04 +/*! Mopidy.js - built 2013-01-16 * http://www.mopidy.com/ - * Copyright (c) 2012 Stein Magnus Jodal and contributors + * Copyright (c) 2013 Stein Magnus Jodal and contributors * Licensed under the Apache License, Version 2.0 */ -function Mopidy(e){this._settings=this._configure(e||{}),this._console=this._getConsole(),this._backoffDelay=this._settings.backoffDelayMin,this._pendingRequests={},this._webSocket=null,bane.createEventEmitter(this),this._delegateEvents(),this._settings.autoConnect&&this.connect()}(typeof define=="function"&&define.amd&&function(e){define(e)}||typeof module=="object"&&function(e){module.exports=e()}||function(e){this.bane=e()})(function(){"use strict";function t(e,t,n){var r,i=n.length;if(i>0){for(r=0;r>>0,o=Math.max(0,Math.min(t,v)),a=[],u=v-o+1,l=[],c=f();if(!o)c.resolve(a);else{d=c.progress,p=function(e){l.push(e),--u||(h=p=w,c.reject(l))},h=function(e){a.push(e),--o||(h=p=w,c.resolve(a))};for(m=0;m>>0,n=[],l=f();if(!s)l.resolve(n);else{u=l.reject,o=function(i,o){r(i,t).then(function(e){n[o]=e,--s||l.resolve(n)},u)};for(a=0;a2;return r(e,function(e){return t.resolve(i?n:e)},t.reject,t.progress)}function y(e,t){var n,r=0;while(n=e[r++])n(t)}function b(e,t){var n,r=t.length;while(r>e){n=t[--r];if(n!=null&&typeof n!="function")throw new Error("arg "+r+" must be a function")}}function w(){}function E(e){return e}var e,t,n;return r.defer=f,r.resolve=i,r.reject=s,r.join=d,r.all=p,r.some=c,r.any=h,r.map=v,r.reduce=m,r.chain=g,r.isPromise=l,o.prototype={always:function(e,t){return this.then(e,e,t)},otherwise:function(e){return this.then(n,e)}},t=[].slice,e=[].reduce||function(e){var t,n,r,i,s;s=0,t=Object(this),i=t.length>>>0,n=arguments;if(n.length<=1)for(;;){if(s in t){r=t[s++];break}if(++s>=i)throw new TypeError}else r=n[1];for(;sthis._settings.backoffDelayMax&&(this._backoffDelay=this._settings.backoffDelayMax)},Mopidy.prototype._resetBackoffDelay=function(){this._backoffDelay=this._settings.backoffDelayMin},Mopidy.prototype.close=function(){this.off("state:offline",this._reconnect),this._webSocket.close()},Mopidy.prototype._handleWebSocketError=function(e){this._console.warn("WebSocket error:",e.stack||e)},Mopidy.prototype._send=function(e){var t=when.defer();switch(this._webSocket.readyState){case WebSocket.CONNECTING:t.resolver.reject({message:"WebSocket is still connecting"});break;case WebSocket.CLOSING:t.resolver.reject({message:"WebSocket is closing"});break;case WebSocket.CLOSED:t.resolver.reject({message:"WebSocket is closed"});break;default:e.jsonrpc="2.0",e.id=this._nextRequestId(),this._pendingRequests[e.id]=t.resolver,this._webSocket.send(JSON.stringify(e)),this.emit("websocket:outgoingMessage",e)}return t.promise},Mopidy.prototype._nextRequestId=function(){var e=-1;return function(){return e+=1,e}}(),Mopidy.prototype._handleMessage=function(e){try{var t=JSON.parse(e.data);t.hasOwnProperty("id")?this._handleResponse(t):t.hasOwnProperty("event")?this._handleEvent(t):this._console.warn("Unknown message type received. Message was: "+e.data)}catch(n){if(!(n instanceof SyntaxError))throw n;this._console.warn("WebSocket message parsing failed. Message was: "+e.data)}},Mopidy.prototype._handleResponse=function(e){if(!this._pendingRequests.hasOwnProperty(e.id)){this._console.warn("Unexpected response received. Message was:",e);return}var t=this._pendingRequests[e.id];delete this._pendingRequests[e.id],e.hasOwnProperty("result")?t.resolve(e.result):e.hasOwnProperty("error")?(t.reject(e.error),this._console.warn("Server returned error:",e.error)):(t.reject({message:"Response without 'result' or 'error' received",data:{response:e}}),this._console.warn("Response without 'result' or 'error' received. Message was:",e))},Mopidy.prototype._handleEvent=function(e){var t=e.event,n=e;delete n.event,this.emit("event:"+this._snakeToCamel(t),n)},Mopidy.prototype._getApiSpec=function(){this._send({method:"core.describe"}).then(this._createApi.bind(this),this._handleWebSocketError).then(null,this._handleWebSocketError)},Mopidy.prototype._createApi=function(e){var t=function(e){return function(){var t=Array.prototype.slice.call(arguments);return this._send({method:e,params:t})}.bind(this)}.bind(this),n=function(e){var t=e.split(".");return t.length>=1&&t[0]==="core"&&(t=t.slice(1)),t},r=function(e){var t=this;return e.forEach(function(e){e=this._snakeToCamel(e),t[e]=t[e]||{},t=t[e]}.bind(this)),t}.bind(this),i=function(i){var s=n(i),o=this._snakeToCamel(s.slice(-1)[0]),u=r(s.slice(0,-1));u[o]=t(i),u[o].description=e[i].description,u[o].params=e[i].params}.bind(this);Object.keys(e).forEach(i),this.emit("state:online")},Mopidy.prototype._snakeToCamel=function(e){return e.replace(/(_[a-z])/g,function(e){return e.toUpperCase().replace("_","")})}; \ No newline at end of file +function Mopidy(e){if(!(this instanceof Mopidy))return new Mopidy(e);this._settings=this._configure(e||{}),this._console=this._getConsole(),this._backoffDelay=this._settings.backoffDelayMin,this._pendingRequests={},this._webSocket=null,bane.createEventEmitter(this),this._delegateEvents(),this._settings.autoConnect&&this.connect()}(typeof define=="function"&&define.amd&&function(e){define(e)}||typeof module=="object"&&function(e){module.exports=e()}||function(e){this.bane=e()})(function(){"use strict";function t(e,t,n){var r,i=n.length;if(i>0){for(r=0;r>>0,o=Math.max(0,Math.min(t,v)),a=[],u=v-o+1,l=[],c=f();if(!o)c.resolve(a);else{d=c.progress,p=function(e){l.push(e),--u||(h=p=w,c.reject(l))},h=function(e){a.push(e),--o||(h=p=w,c.resolve(a))};for(m=0;m>>0,n=[],l=f();if(!s)l.resolve(n);else{u=l.reject,o=function(i,o){r(i,t).then(function(e){n[o]=e,--s||l.resolve(n)},u)};for(a=0;a2;return r(e,function(e){return t.resolve(i?n:e)},t.reject,t.progress)}function y(e,t){var n,r=0;while(n=e[r++])n(t)}function b(e,t){var n,r=t.length;while(r>e){n=t[--r];if(n!=null&&typeof n!="function")throw new Error("arg "+r+" must be a function")}}function w(){}function E(e){return e}var e,t,n;return r.defer=f,r.resolve=i,r.reject=s,r.join=d,r.all=p,r.some=c,r.any=h,r.map=v,r.reduce=m,r.chain=g,r.isPromise=l,o.prototype={always:function(e,t){return this.then(e,e,t)},otherwise:function(e){return this.then(n,e)}},t=[].slice,e=[].reduce||function(e){var t,n,r,i,s;s=0,t=Object(this),i=t.length>>>0,n=arguments;if(n.length<=1)for(;;){if(s in t){r=t[s++];break}if(++s>=i)throw new TypeError}else r=n[1];for(;sthis._settings.backoffDelayMax&&(this._backoffDelay=this._settings.backoffDelayMax)},Mopidy.prototype._resetBackoffDelay=function(){this._backoffDelay=this._settings.backoffDelayMin},Mopidy.prototype.close=function(){this.off("state:offline",this._reconnect),this._webSocket.close()},Mopidy.prototype._handleWebSocketError=function(e){this._console.warn("WebSocket error:",e.stack||e)},Mopidy.prototype._send=function(e){var t=when.defer();switch(this._webSocket.readyState){case WebSocket.CONNECTING:t.resolver.reject({message:"WebSocket is still connecting"});break;case WebSocket.CLOSING:t.resolver.reject({message:"WebSocket is closing"});break;case WebSocket.CLOSED:t.resolver.reject({message:"WebSocket is closed"});break;default:e.jsonrpc="2.0",e.id=this._nextRequestId(),this._pendingRequests[e.id]=t.resolver,this._webSocket.send(JSON.stringify(e)),this.emit("websocket:outgoingMessage",e)}return t.promise},Mopidy.prototype._nextRequestId=function(){var e=-1;return function(){return e+=1,e}}(),Mopidy.prototype._handleMessage=function(e){try{var t=JSON.parse(e.data);t.hasOwnProperty("id")?this._handleResponse(t):t.hasOwnProperty("event")?this._handleEvent(t):this._console.warn("Unknown message type received. Message was: "+e.data)}catch(n){if(!(n instanceof SyntaxError))throw n;this._console.warn("WebSocket message parsing failed. Message was: "+e.data)}},Mopidy.prototype._handleResponse=function(e){if(!this._pendingRequests.hasOwnProperty(e.id)){this._console.warn("Unexpected response received. Message was:",e);return}var t=this._pendingRequests[e.id];delete this._pendingRequests[e.id],e.hasOwnProperty("result")?t.resolve(e.result):e.hasOwnProperty("error")?(t.reject(e.error),this._console.warn("Server returned error:",e.error)):(t.reject({message:"Response without 'result' or 'error' received",data:{response:e}}),this._console.warn("Response without 'result' or 'error' received. Message was:",e))},Mopidy.prototype._handleEvent=function(e){var t=e.event,n=e;delete n.event,this.emit("event:"+this._snakeToCamel(t),n)},Mopidy.prototype._getApiSpec=function(){this._send({method:"core.describe"}).then(this._createApi.bind(this),this._handleWebSocketError).then(null,this._handleWebSocketError)},Mopidy.prototype._createApi=function(e){var t=function(e){return function(){var t=Array.prototype.slice.call(arguments);return this._send({method:e,params:t})}.bind(this)}.bind(this),n=function(e){var t=e.split(".");return t.length>=1&&t[0]==="core"&&(t=t.slice(1)),t},r=function(e){var t=this;return e.forEach(function(e){e=this._snakeToCamel(e),t[e]=t[e]||{},t=t[e]}.bind(this)),t}.bind(this),i=function(i){var s=n(i),o=this._snakeToCamel(s.slice(-1)[0]),u=r(s.slice(0,-1));u[o]=t(i),u[o].description=e[i].description,u[o].params=e[i].params}.bind(this);Object.keys(e).forEach(i),this.emit("state:online")},Mopidy.prototype._snakeToCamel=function(e){return e.replace(/(_[a-z])/g,function(e){return e.toUpperCase().replace("_","")})}; \ No newline at end of file From befc83f28c82d847b1b23da6235f08e010dd41ac Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 12 Mar 2013 20:13:54 +0100 Subject: [PATCH 24/36] Update changelog --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 730cf275..905bbc0f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,7 +5,7 @@ Changes This change log is used to track all major changes to Mopidy. -v0.11.2 (UNRELEASED) +v0.12.0 (UNRELEASED) ==================== (in development) From 1abab8c90724eb39f982ae10412a638bcd414b76 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 8 Jan 2013 23:20:41 +0100 Subject: [PATCH 25/36] tests: Pass audio actor proxy to backends not audio module --- tests/backends/base/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/backends/base/events.py b/tests/backends/base/events.py index 0a2e6722..2b6df347 100644 --- a/tests/backends/base/events.py +++ b/tests/backends/base/events.py @@ -11,7 +11,7 @@ from mopidy.backends import listener class BackendEventsTest(object): def setUp(self): self.audio = mock.Mock(spec=audio.Audio) - self.backend = self.backend_class.start(audio=audio).proxy() + self.backend = self.backend_class.start(audio=self.audio).proxy() self.core = core.Core.start(backends=[self.backend]).proxy() def tearDown(self): From 795926cfa80ddd2a4a405570f7f50959fc9dd720 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 27 Dec 2012 03:21:20 +0100 Subject: [PATCH 26/36] stream backend: Add StreamBackend, fixes #151 Adds a basic streaming backend simply handles streaming audio and nothing else. I.e. no metadata beyond the URI we where given. #270 still needs to be solved for actual metadata to make sense in this backend. --- mopidy/backends/stream/__init__.py | 23 ++++++++++++ mopidy/backends/stream/actor.py | 57 ++++++++++++++++++++++++++++++ mopidy/settings.py | 25 +++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 mopidy/backends/stream/__init__.py create mode 100644 mopidy/backends/stream/actor.py diff --git a/mopidy/backends/stream/__init__.py b/mopidy/backends/stream/__init__.py new file mode 100644 index 00000000..82755540 --- /dev/null +++ b/mopidy/backends/stream/__init__.py @@ -0,0 +1,23 @@ +"""A backend for playing music for streaming music. + +This backend will handle streaming of URIs in +:attr:`mopidy.settings.STREAM_PROTOCOLS` assuming the right plugins are +installed. + +**Issues:** + +https://github.com/mopidy/mopidy/issues?labels=Stream+backend + +**Dependencies:** + +- None + +**Settings:** + +- :attr:`mopidy.settings.STREAM_PROTOCOLS` +""" + +from __future__ import unicode_literals + +# flake8: noqa +from .actor import StreamBackend diff --git a/mopidy/backends/stream/actor.py b/mopidy/backends/stream/actor.py new file mode 100644 index 00000000..7fc28711 --- /dev/null +++ b/mopidy/backends/stream/actor.py @@ -0,0 +1,57 @@ +from __future__ import unicode_literals + +import pygst +pygst.require('0.10') +import gst + +import logging +import urlparse + +import pykka + +from mopidy import settings +from mopidy.backends import base +from mopidy.models import SearchResult, Track + +logger = logging.getLogger('mopidy.backends.stream') + + +class StreamBackend(pykka.ThreadingActor, base.Backend): + def __init__(self, audio): + super(StreamBackend, self).__init__() + + self.library = StreamLibraryProvider(backend=self) + self.playback = base.BasePlaybackProvider(audio=audio, backend=self) + self.playlists = None + + available_protocols = set() + + registry = gst.registry_get_default() + for factory in registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY): + for uri in factory.get_uri_protocols(): + if uri in settings.STREAM_PROTOCOLS: + available_protocols.add(uri) + + self.uri_schemes = list(available_protocols) + + +# TODO: Should we consider letting lookup know how to expand common playlist +# formats (m3u, pls, etc) for http(s) URIs? +class StreamLibraryProvider(base.BaseLibraryProvider): + def lookup(self, uri): + if urlparse.urlsplit(uri).scheme not in self.backend.uri_schemes: + return [] + # TODO: actually lookup the stream metadata by getting tags in same + # way as we do for updating the local library with mopidy.scanner + # Note that we would only want the stream metadata at this stage, + # not the currently playing track's. + return [Track(uri=uri, name=uri)] + + def find_exact(self, **query): + return SearchResult() + + def search(self, **query): + return SearchResult() + + def refresh(self, uri=None): + pass diff --git a/mopidy/settings.py b/mopidy/settings.py index c2081e27..9d99a7cb 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -20,10 +20,12 @@ from __future__ import unicode_literals #: BACKENDS = ( #: u'mopidy.backends.local.LocalBackend', #: u'mopidy.backends.spotify.SpotifyBackend', +#: u'mopidy.backends.spotify.StreamBackend', #: ) BACKENDS = ( 'mopidy.backends.local.LocalBackend', 'mopidy.backends.spotify.SpotifyBackend', + 'mopidy.backends.stream.StreamBackend', ) #: The log format used for informational logging. @@ -301,3 +303,26 @@ SPOTIFY_PROXY_PASSWORD = None #: #: SPOTIFY_TIMEOUT = 10 SPOTIFY_TIMEOUT = 10 + +#: Whitelist of URIs to support streaming from. +#: +#: Used by :mod:`mopidy.backends.stream` +#: +#: Default:: +#: +#: STREAM_PROTOCOLS = ( +#: u'http', +#: u'https', +#: u'mms', +#: u'rtmp', +#: u'rtmps', +#: u'rtsp', +#: ) +STREAM_PROTOCOLS = ( + 'http', + 'https', + 'mms', + 'rtmp', + 'rtmps', + 'rtsp', +) From 6a0e80a5c3efcc6103afea143559b6cc213170c4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 1 Jan 2013 16:56:34 +0100 Subject: [PATCH 27/36] core: Update BaseLibraryProvider to not require refresh, search or find_exact. These methods may now return None, and the core code has been updated to filter out missing SearchResults. --- mopidy/backends/base.py | 12 ++++++------ mopidy/backends/stream/actor.py | 9 --------- mopidy/core/library.py | 4 ++-- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/mopidy/backends/base.py b/mopidy/backends/base.py index 8250a24c..f49aa89b 100644 --- a/mopidy/backends/base.py +++ b/mopidy/backends/base.py @@ -57,9 +57,9 @@ class BaseLibraryProvider(object): """ See :meth:`mopidy.core.LibraryController.find_exact`. - *MUST be implemented by subclass.* + *MAY be implemented by subclass.* """ - raise NotImplementedError + pass def lookup(self, uri): """ @@ -73,17 +73,17 @@ class BaseLibraryProvider(object): """ See :meth:`mopidy.core.LibraryController.refresh`. - *MUST be implemented by subclass.* + *MAY be implemented by subclass.* """ - raise NotImplementedError + pass def search(self, **query): """ See :meth:`mopidy.core.LibraryController.search`. - *MUST be implemented by subclass.* + *MAY be implemented by subclass.* """ - raise NotImplementedError + pass class BasePlaybackProvider(object): diff --git a/mopidy/backends/stream/actor.py b/mopidy/backends/stream/actor.py index 7fc28711..cdf777af 100644 --- a/mopidy/backends/stream/actor.py +++ b/mopidy/backends/stream/actor.py @@ -46,12 +46,3 @@ class StreamLibraryProvider(base.BaseLibraryProvider): # Note that we would only want the stream metadata at this stage, # not the currently playing track's. return [Track(uri=uri, name=uri)] - - def find_exact(self, **query): - return SearchResult() - - def search(self, **query): - return SearchResult() - - def refresh(self, uri=None): - pass diff --git a/mopidy/core/library.py b/mopidy/core/library.py index 39a1e99c..e4be7ce8 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -41,7 +41,7 @@ class LibraryController(object): query = query or kwargs futures = [ b.library.find_exact(**query) for b in self.backends.with_library] - return pykka.get_all(futures) + return [result for result in pykka.get_all(futures) if result] def lookup(self, uri): """ @@ -101,4 +101,4 @@ class LibraryController(object): query = query or kwargs futures = [ b.library.search(**query) for b in self.backends.with_library] - return pykka.get_all(futures) + return [result for result in pykka.get_all(futures) if result] From 692bcae8e46f8b6a951fa711f7cc771990605f33 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 1 Jan 2013 17:03:48 +0100 Subject: [PATCH 28/36] core: Update tests with cases for filtering out None results. --- tests/core/library_test.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/core/library_test.py b/tests/core/library_test.py index 32e618d2..e01696c7 100644 --- a/tests/core/library_test.py +++ b/tests/core/library_test.py @@ -90,6 +90,22 @@ class CoreLibraryTest(unittest.TestCase): self.library1.find_exact.assert_called_once_with(any=['a']) self.library2.find_exact.assert_called_once_with(any=['a']) + def test_find_exact_filters_out_none(self): + track1 = Track(uri='dummy1:a') + result1 = SearchResult(tracks=[track1]) + + self.library1.find_exact().get.return_value = result1 + self.library1.find_exact.reset_mock() + self.library2.find_exact().get.return_value = None + self.library2.find_exact.reset_mock() + + result = self.core.library.find_exact(any=['a']) + + self.assertIn(result1, result) + self.assertNotIn(None, result) + self.library1.find_exact.assert_called_once_with(any=['a']) + self.library2.find_exact.assert_called_once_with(any=['a']) + def test_find_accepts_query_dict_instead_of_kwargs(self): track1 = Track(uri='dummy1:a') track2 = Track(uri='dummy2:a') @@ -126,6 +142,22 @@ class CoreLibraryTest(unittest.TestCase): self.library1.search.assert_called_once_with(any=['a']) self.library2.search.assert_called_once_with(any=['a']) + def test_search_filters_out_none(self): + track1 = Track(uri='dummy1:a') + result1 = SearchResult(tracks=[track1]) + + self.library1.search().get.return_value = result1 + self.library1.search.reset_mock() + self.library2.search().get.return_value = None + self.library2.search.reset_mock() + + result = self.core.library.search(any=['a']) + + self.assertIn(result1, result) + self.assertNotIn(None, result) + self.library1.search.assert_called_once_with(any=['a']) + self.library2.search.assert_called_once_with(any=['a']) + def test_search_accepts_query_dict_instead_of_kwargs(self): track1 = Track(uri='dummy1:a') track2 = Track(uri='dummy2:a') From 9314f2df5c6b434982df7a0156b1e5257fd07000 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 1 Jan 2013 17:25:07 +0100 Subject: [PATCH 29/36] audio: Move supported URI checking to mopidy.audio.utils In order to avoid gstreamer imports leaking into more of our code I'm moving this to a new utils class in audio. --- mopidy/audio/utils.py | 23 +++++++++++++++++++++++ mopidy/backends/stream/actor.py | 16 +++------------- 2 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 mopidy/audio/utils.py diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py new file mode 100644 index 00000000..3f5f685e --- /dev/null +++ b/mopidy/audio/utils.py @@ -0,0 +1,23 @@ +from __future__ import unicode_literals + +import pygst +pygst.require('0.10') +import gst + + +def supported_uri_schemes(uri_schemes): + """Determine which URIs we can actually support from provided whitelist. + + :param uri_schemes: list/set of URIs to check support for. + :type uri_schemes: list or set or URI schemes as strings. + :rtype: set of URI schemes we can support via this GStreamer install. + """ + supported_schemes= set() + registry = gst.registry_get_default() + + for factory in registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY): + for uri in factory.get_uri_protocols(): + if uri in uri_schemes: + supported_schemes.add(uri) + + return supported_schemes diff --git a/mopidy/backends/stream/actor.py b/mopidy/backends/stream/actor.py index cdf777af..0c91f291 100644 --- a/mopidy/backends/stream/actor.py +++ b/mopidy/backends/stream/actor.py @@ -1,15 +1,12 @@ from __future__ import unicode_literals -import pygst -pygst.require('0.10') -import gst - import logging import urlparse import pykka from mopidy import settings +from mopidy.audio import utils from mopidy.backends import base from mopidy.models import SearchResult, Track @@ -24,15 +21,8 @@ class StreamBackend(pykka.ThreadingActor, base.Backend): self.playback = base.BasePlaybackProvider(audio=audio, backend=self) self.playlists = None - available_protocols = set() - - registry = gst.registry_get_default() - for factory in registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY): - for uri in factory.get_uri_protocols(): - if uri in settings.STREAM_PROTOCOLS: - available_protocols.add(uri) - - self.uri_schemes = list(available_protocols) + self.uri_schemes = utils.supported_uri_schemes( + settings.STREAM_PROTOCOLS) # TODO: Should we consider letting lookup know how to expand common playlist From 3e39469cbbb00353a1e7fb927c7018f0037419ed Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Jan 2013 22:29:07 +0100 Subject: [PATCH 30/36] docs: Document new backend --- docs/api/backends.rst | 3 ++- docs/modules/backends/stream.rst | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 docs/modules/backends/stream.rst diff --git a/docs/api/backends.rst b/docs/api/backends.rst index f0aadd53..32c04d37 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends.rst @@ -46,5 +46,6 @@ Backend implementations ======================= * :mod:`mopidy.backends.dummy` -* :mod:`mopidy.backends.spotify` * :mod:`mopidy.backends.local` +* :mod:`mopidy.backends.spotify` +* :mod:`mopidy.backends.stream` diff --git a/docs/modules/backends/stream.rst b/docs/modules/backends/stream.rst new file mode 100644 index 00000000..73e53048 --- /dev/null +++ b/docs/modules/backends/stream.rst @@ -0,0 +1,7 @@ +*********************************************** +:mod:`mopidy.backends.stream` -- Stream backend +*********************************************** + +.. automodule:: mopidy.backends.stream + :synopsis: Backend for playing audio streams + :members: From c4d94988a3e4ba27a164ad82bf9f888d7ee73547 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Jan 2013 22:32:58 +0100 Subject: [PATCH 31/36] docs: Add stream backend to changelog Conflicts: docs/changes.rst --- docs/changes.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 905bbc0f..8d3eba7a 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -26,6 +26,23 @@ v0.12.0 (UNRELEASED) - Make ``mopidy-scan`` support symlinks. +**Stream backend** + +We've added a new backend for playing audio streams, the :mod:`stream backend +`. It is activated by default. + +The stream backend supports the intersection of what your GStreamer +installation supports and what protocols are included in the +:attr:`mopidy.settings.STREAM_PROTOCOLS` settings. + +Current limitations: + +- No metadata about the current track in the stream is available. + +- Playlists are not parsed, so you can't play e.g. a M3U or PLS file which + contains stream URIs. You need to extract the stream URL from the playlist + yourself. See :issue:`303` for progress on this. + **Core API** - :meth:`mopidy.core.PlaylistsController.get_playlists` now accepts an argument From 6b052165d83139db28c2da175aee1e3989e7e90b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Jan 2013 22:35:43 +0100 Subject: [PATCH 32/36] docs: Add period for consistency --- mopidy/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/settings.py b/mopidy/settings.py index 9d99a7cb..6ee9357e 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -306,7 +306,7 @@ SPOTIFY_TIMEOUT = 10 #: Whitelist of URIs to support streaming from. #: -#: Used by :mod:`mopidy.backends.stream` +#: Used by :mod:`mopidy.backends.stream`. #: #: Default:: #: From 57a287ecea05dac684c40d468794c9da4f6f2d24 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Jan 2013 22:36:29 +0100 Subject: [PATCH 33/36] docs: Add more periods for consistency --- mopidy/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/settings.py b/mopidy/settings.py index 6ee9357e..fd3dfd6f 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -288,7 +288,7 @@ SPOTIFY_PROXY_USERNAME = None #: Spotify proxy password. #: -#: Used by :mod:`mopidy.backends.spotify` +#: Used by :mod:`mopidy.backends.spotify`. #: #: Default:: #: @@ -297,7 +297,7 @@ SPOTIFY_PROXY_PASSWORD = None #: Max number of seconds to wait for Spotify operations to complete. #: -#: Used by :mod:`mopidy.backends.spotify` +#: Used by :mod:`mopidy.backends.spotify`. #: #: Default:: #: From 8fe688f7a73976c1514a668e3dc5f30a3052c63e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Jan 2013 00:05:27 +0100 Subject: [PATCH 34/36] docs: Fix typos in changelog Conflicts: docs/changes.rst --- docs/changes.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 8d3eba7a..a325002e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -29,11 +29,9 @@ v0.12.0 (UNRELEASED) **Stream backend** We've added a new backend for playing audio streams, the :mod:`stream backend -`. It is activated by default. - -The stream backend supports the intersection of what your GStreamer -installation supports and what protocols are included in the -:attr:`mopidy.settings.STREAM_PROTOCOLS` settings. +`. It is activated by default. The stream backend +supports the intersection of what your GStreamer installation supports and what +protocols are included in the :attr:`mopidy.settings.STREAM_PROTOCOLS` setting. Current limitations: From 0318e6e47ce6ee08ad46ba07ceadbda350d4c2c5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 12 Mar 2013 21:02:47 +0100 Subject: [PATCH 35/36] Bump version number to 0.12.0 --- mopidy/__init__.py | 2 +- tests/version_test.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 9d66b722..0a8e68aa 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -23,7 +23,7 @@ if (isinstance(pykka.__version__, basestring) warnings.filterwarnings('ignore', 'could not open display') -__version__ = '0.11.1' +__version__ = '0.12.0' from mopidy import settings as default_settings_module diff --git a/tests/version_test.py b/tests/version_test.py index 1cb3967c..a444ea1f 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -34,5 +34,6 @@ class VersionTest(unittest.TestCase): self.assertLess(SV('0.8.1'), SV('0.9.0')) self.assertLess(SV('0.9.0'), SV('0.10.0')) self.assertLess(SV('0.10.0'), SV('0.11.0')) - self.assertLess(SV('0.11.0'), SV(__version__)) - self.assertLess(SV(__version__), SV('0.11.2')) + self.assertLess(SV('0.11.0'), SV('0.11.1')) + self.assertLess(SV('0.11.1'), SV(__version__)) + self.assertLess(SV(__version__), SV('0.12.1')) From af6b661ef7bffdfa81029fc9513c1e91c019a384 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 12 Mar 2013 21:02:57 +0100 Subject: [PATCH 36/36] docs: Update changelog for v0.12.0 --- docs/changes.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index a325002e..a04de250 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,10 +5,15 @@ Changes This change log is used to track all major changes to Mopidy. -v0.12.0 (UNRELEASED) +v0.12.0 (2013-03-12) ==================== -(in development) +The 0.12 release has been delayed for a while because of some issues related +some ongoing GStreamer cleanup we didn't invest enough time to finish. Finally, +we've come to our senses and have now cherry-picked the good parts to bring you +a new release, while postponing the GStreamer changes to 0.13. The release adds +a new backend for playing audio streams, as well as various minor improvements +throughout Mopidy. - Make Mopidy work on early Python 2.6 versions. (Fixes: :issue:`302`)