From 960859aebfe25e1cb032e437b856aec76c5b5a38 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 6 Nov 2011 22:04:23 +0100 Subject: [PATCH 01/60] Add unittest2 to test requirements, as we depend on it for Python 2.6 --- requirements/tests.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/tests.txt b/requirements/tests.txt index 922ef6dc..e24edd3c 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -2,4 +2,5 @@ coverage mock >= 0.7 nose tox +unittest2 yappi From c7207c3d85cf8ea4c4c3bf8a4fa7703628827233 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Nov 2011 23:05:09 +0100 Subject: [PATCH 02/60] Break too long line --- mopidy/backends/spotify/playlist_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/backends/spotify/playlist_manager.py b/mopidy/backends/spotify/playlist_manager.py index f72ac4ca..05f9514d 100644 --- a/mopidy/backends/spotify/playlist_manager.py +++ b/mopidy/backends/spotify/playlist_manager.py @@ -27,7 +27,8 @@ class SpotifyPlaylistManager(PyspotifyPlaylistManager): def tracks_removed(self, playlist, tracks, userdata): """Callback used by pyspotify""" logger.debug(u'Callback called: ' - u'%d track(s) removed from playlist "%s"', len(tracks), playlist.name()) + u'%d track(s) removed from playlist "%s"', + len(tracks), playlist.name()) self.session_manager.refresh_stored_playlists() def playlist_renamed(self, playlist, userdata): From fab020f2d0b202634c4f4ddfc4296eafc669e071 Mon Sep 17 00:00:00 2001 From: sandos Date: Mon, 12 Dec 2011 22:22:03 +0100 Subject: [PATCH 03/60] performance of playlistinfo and status not dependent on playlist length --- mopidy/backends/base/current_playlist.py | 10 ++++++++++ mopidy/frontends/mpd/protocol/current_playlist.py | 6 ++++++ mopidy/frontends/mpd/protocol/status.py | 4 ++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index 17125ac0..367f9c5d 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -1,6 +1,7 @@ from copy import copy import logging import random +import pprint from mopidy.listeners import BackendListener from mopidy.models import CpTrack @@ -28,6 +29,7 @@ class CurrentPlaylistController(object): Read-only. """ + logger.debug(u'current_playlist.cp_tracks') return [copy(ct) for ct in self._cp_tracks] @property @@ -37,8 +39,16 @@ class CurrentPlaylistController(object): Read-only. """ + logger.debug(u'current_playlist.tracks()') return [ct[1] for ct in self._cp_tracks] + @property + def tracks_len(self): + """ + Length of current playlist + """ + return len(self._cp_tracks) + @property def version(self): """ diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index c7136804..c566bf7e 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -254,6 +254,12 @@ def playlistinfo(context, songpos=None, end = songpos + 1 if start == -1: end = None + else: + #Fetch one single track, hot code path (avoid deep-copying the entire playlist) + res = context.backend.current_playlist.get(cpid=songpos).get() + cpids = [res.cpid] + l = [res.track] + return tracks_to_mpd_format(l, 0, 1, cpids=cpids) cpids = [ct[0] for ct in context.backend.current_playlist.cp_tracks.get()] return tracks_to_mpd_format( diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 20a66775..b0bc7ad7 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -166,7 +166,7 @@ def status(context): decimal places for millisecond precision. """ futures = { - 'current_playlist.tracks': context.backend.current_playlist.tracks, + 'current_playlist.tracks_len': context.backend.current_playlist.tracks_len, 'current_playlist.version': context.backend.current_playlist.version, 'mixer.volume': context.mixer.volume, 'playback.consume': context.backend.playback.consume, @@ -213,7 +213,7 @@ def _status_consume(futures): return 0 def _status_playlist_length(futures): - return len(futures['current_playlist.tracks'].get()) + return futures['current_playlist.tracks_len'].get() def _status_playlist_version(futures): return futures['current_playlist.version'].get() From 1414c2394b1f46ce6c0f2c5825353cbb9e099d66 Mon Sep 17 00:00:00 2001 From: sandos Date: Mon, 12 Dec 2011 22:23:28 +0100 Subject: [PATCH 04/60] Remove unused import --- mopidy/backends/base/current_playlist.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index 367f9c5d..85c4e135 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -1,7 +1,6 @@ from copy import copy import logging import random -import pprint from mopidy.listeners import BackendListener from mopidy.models import CpTrack From 5b1d77e79f27acb77c3339e1f1ae28b9071cfbcd Mon Sep 17 00:00:00 2001 From: sandos Date: Mon, 12 Dec 2011 22:26:33 +0100 Subject: [PATCH 05/60] Remove some logging --- mopidy/backends/base/current_playlist.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index 85c4e135..1134984b 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -28,7 +28,6 @@ class CurrentPlaylistController(object): Read-only. """ - logger.debug(u'current_playlist.cp_tracks') return [copy(ct) for ct in self._cp_tracks] @property @@ -38,7 +37,6 @@ class CurrentPlaylistController(object): Read-only. """ - logger.debug(u'current_playlist.tracks()') return [ct[1] for ct in self._cp_tracks] @property From c5a4bb0e229a070192d40d068b426aa8e9e7eddb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 27 Dec 2011 22:22:30 +0100 Subject: [PATCH 06/60] Rename tracks_len to length, and add test --- mopidy/backends/base/current_playlist.py | 4 ++-- mopidy/frontends/mpd/protocol/status.py | 4 ++-- tests/backends/base/current_playlist.py | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index 1134984b..966456a4 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -40,9 +40,9 @@ class CurrentPlaylistController(object): return [ct[1] for ct in self._cp_tracks] @property - def tracks_len(self): + def length(self): """ - Length of current playlist + Length of the current playlist. """ return len(self._cp_tracks) diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index b0bc7ad7..f4d66c56 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -166,7 +166,7 @@ def status(context): decimal places for millisecond precision. """ futures = { - 'current_playlist.tracks_len': context.backend.current_playlist.tracks_len, + 'current_playlist.length': context.backend.current_playlist.length, 'current_playlist.version': context.backend.current_playlist.version, 'mixer.volume': context.mixer.volume, 'playback.consume': context.backend.playback.consume, @@ -213,7 +213,7 @@ def _status_consume(futures): return 0 def _status_playlist_length(futures): - return futures['current_playlist.tracks_len'].get() + return futures['current_playlist.length'].get() def _status_playlist_version(futures): return futures['current_playlist.version'].get() diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py index c81f4a0d..6d4854a7 100644 --- a/tests/backends/base/current_playlist.py +++ b/tests/backends/base/current_playlist.py @@ -18,6 +18,13 @@ class CurrentPlaylistControllerTest(object): assert len(self.tracks) == 3, 'Need three tracks to run tests.' + def test_length(self): + self.assertEqual(0, len(self.controller.cp_tracks)) + self.assertEqual(0, self.controller.length) + self.controller.append(self.tracks) + self.assertEqual(3, len(self.controller.cp_tracks)) + self.assertEqual(3, self.controller.length) + def test_add(self): for track in self.tracks: cp_track = self.controller.add(track) From aeee5518ac9c96c2728dd24291142da21c03a307 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 27 Dec 2011 23:57:03 +0100 Subject: [PATCH 07/60] Improved and simplified the 'playlistinfo' command handler Cleaning up the rest of the code, it became obvious that sandos' performance patch did not alter the semantics of 'playlistinfo'. --- .../mpd/protocol/current_playlist.py | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index c566bf7e..14816024 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -255,32 +255,26 @@ def playlistinfo(context, songpos=None, if start == -1: end = None else: - #Fetch one single track, hot code path (avoid deep-copying the entire playlist) - res = context.backend.current_playlist.get(cpid=songpos).get() - cpids = [res.cpid] - l = [res.track] - return tracks_to_mpd_format(l, 0, 1, cpids=cpids) - cpids = [ct[0] for ct in - context.backend.current_playlist.cp_tracks.get()] - return tracks_to_mpd_format( - context.backend.current_playlist.tracks.get(), - start, end, cpids=cpids) + # Hot code path: Fetch a single track, while avoiding deep-copying + # the entire playlist + cp_track = context.backend.current_playlist.get(cpid=songpos).get() + cpids = [cp_track.cpid] + tracks = [cp_track.track] + return tracks_to_mpd_format(tracks, 0, 1, cpids=cpids) else: if start is None: start = 0 start = int(start) - if not (0 <= start <= len( - context.backend.current_playlist.tracks.get())): + if not (0 <= start <= context.backend.current_playlist.length.get()): raise MpdArgError(u'Bad song index', command=u'playlistinfo') if end is not None: end = int(end) - if end > len(context.backend.current_playlist.tracks.get()): + if end > context.backend.current_playlist.length.get(): end = None - cpids = [ct[0] for ct in - context.backend.current_playlist.cp_tracks.get()] - return tracks_to_mpd_format( - context.backend.current_playlist.tracks.get(), - start, end, cpids=cpids) + cp_tracks = context.backend.current_playlist.cp_tracks.get() + cpids = [cp_track.cpid for cp_track in cp_tracks] + tracks = [cp_track.track for cp_track in cp_tracks] + return tracks_to_mpd_format(tracks, start, end, cpids=cpids) @handle_request(r'^playlistsearch "(?P[^"]+)" "(?P[^"]+)"$') @handle_request(r'^playlistsearch (?P\S+) "(?P[^"]+)"$') From e0e3a1c518742eca42a47040bd0cbae1f1ed1ee1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 28 Dec 2011 00:05:28 +0100 Subject: [PATCH 08/60] Update changelog --- docs/changes.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index d39d6fc2..ac2c21bf 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -18,6 +18,13 @@ v0.7.0 (in development) if you use playlist folders, you will no longer get lots of log messages about bad playlists. +- Added the method + :meth:`mopidy.backends.base.CurrentPlaylistController.length()` to reduce the + need for copying the entire current playlist from one thread to another. + Thanks to John Bäckstrand. + +- The MPD command ``playlistinfo`` is now faster, thanks to John Bäckstrand. + v0.6.0 (2011-10-09) =================== From 6f6e2c7fd7ae7807591af88d17d2c2a502b631d8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 28 Dec 2011 00:30:38 +0100 Subject: [PATCH 09/60] Let track{,s}_to_mpd_format() understand CpTrack objects Thus the cpid and cpids kwargs can be removed, and lots of code doing formatting of MPD responses can be simplified. This also reduces the need for making full copies of the current playlist, which improves performance. --- .../mpd/protocol/current_playlist.py | 24 ++++++----------- mopidy/frontends/mpd/protocol/status.py | 8 +++--- mopidy/frontends/mpd/translator.py | 26 ++++++++++--------- tests/frontends/mpd/serializer_test.py | 8 +++--- 4 files changed, 30 insertions(+), 36 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 14816024..b04c86d0 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -1,7 +1,8 @@ from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError, MpdNotImplemented) from mopidy.frontends.mpd.protocol import handle_request -from mopidy.frontends.mpd.translator import tracks_to_mpd_format +from mopidy.frontends.mpd.translator import (track_to_mpd_format, + tracks_to_mpd_format) @handle_request(r'^add "(?P[^"]*)"$') def add(context, uri): @@ -193,10 +194,9 @@ def playlistfind(context, tag, needle): if tag == 'filename': try: cp_track = context.backend.current_playlist.get(uri=needle).get() - (cpid, track) = cp_track position = context.backend.current_playlist.cp_tracks.get().index( cp_track) - return track.mpd_format(cpid=cpid, position=position) + return track_to_mpd_format(cp_track, position=position) except LookupError: return None raise MpdNotImplemented # TODO @@ -217,14 +217,12 @@ def playlistid(context, cpid=None): cp_track = context.backend.current_playlist.get(cpid=cpid).get() position = context.backend.current_playlist.cp_tracks.get().index( cp_track) - return cp_track.track.mpd_format(position=position, cpid=cpid) + return track_to_mpd_format(cp_track, position=position) except LookupError: raise MpdNoExistError(u'No such song', command=u'playlistid') else: - cpids = [ct[0] for ct in - context.backend.current_playlist.cp_tracks.get()] return tracks_to_mpd_format( - context.backend.current_playlist.tracks.get(), cpids=cpids) + context.backend.current_playlist.cp_tracks.get()) @handle_request(r'^playlistinfo$') @handle_request(r'^playlistinfo "(?P-?\d+)"$') @@ -258,9 +256,7 @@ def playlistinfo(context, songpos=None, # Hot code path: Fetch a single track, while avoiding deep-copying # the entire playlist cp_track = context.backend.current_playlist.get(cpid=songpos).get() - cpids = [cp_track.cpid] - tracks = [cp_track.track] - return tracks_to_mpd_format(tracks, 0, 1, cpids=cpids) + return tracks_to_mpd_format([cp_track], 0, 1) else: if start is None: start = 0 @@ -272,9 +268,7 @@ def playlistinfo(context, songpos=None, if end > context.backend.current_playlist.length.get(): end = None cp_tracks = context.backend.current_playlist.cp_tracks.get() - cpids = [cp_track.cpid for cp_track in cp_tracks] - tracks = [cp_track.track for cp_track in cp_tracks] - return tracks_to_mpd_format(tracks, start, end, cpids=cpids) + return tracks_to_mpd_format(cp_tracks, start, end) @handle_request(r'^playlistsearch "(?P[^"]+)" "(?P[^"]+)"$') @handle_request(r'^playlistsearch (?P\S+) "(?P[^"]+)"$') @@ -313,10 +307,8 @@ def plchanges(context, version): """ # XXX Naive implementation that returns all tracks as changed if int(version) < context.backend.current_playlist.version: - cpids = [ct[0] for ct in - context.backend.current_playlist.cp_tracks.get()] return tracks_to_mpd_format( - context.backend.current_playlist.tracks.get(), cpids=cpids) + context.backend.current_playlist.cp_tracks.get()) @handle_request(r'^plchangesposid "(?P\d+)"$') def plchangesposid(context, version): diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index f4d66c56..f32c46c8 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -1,8 +1,9 @@ import pykka.future from mopidy.backends.base import PlaybackController -from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.exceptions import MpdNotImplemented +from mopidy.frontends.mpd.protocol import handle_request +from mopidy.frontends.mpd.translator import track_to_mpd_format #: Subsystems that can be registered with idle command. SUBSYSTEMS = ['database', 'mixer', 'options', 'output', @@ -32,9 +33,8 @@ def currentsong(context): """ current_cp_track = context.backend.playback.current_cp_track.get() if current_cp_track is not None: - return current_cp_track.track.mpd_format( - position=context.backend.playback.current_playlist_position.get(), - cpid=current_cp_track.cpid) + position = context.backend.playback.current_playlist_position.get() + return track_to_mpd_format(current_cp_track, position=position) @handle_request(r'^idle$') @handle_request(r'^idle (?P.+)$') diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 562b2d2d..6ae32c9e 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -2,26 +2,28 @@ 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 uri_to_path, split_path +from mopidy.models import CpTrack +from mopidy.utils.path import mtime as get_mtime, uri_to_path, split_path -def track_to_mpd_format(track, position=None, cpid=None): +def track_to_mpd_format(track, position=None): """ Format track for output to MPD client. :param track: the track - :type track: :class:`mopidy.models.Track` + :type track: :class:`mopidy.models.Track` or :class:`mopidy.models.CpTrack` :param position: track's position in playlist :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 """ + if isinstance(track, CpTrack): + (cpid, track) = track + else: + (cpid, track) = (None, track) result = [ ('file', track.uri or ''), ('Time', track.length and (track.length // 1000) or 0), @@ -88,14 +90,15 @@ def artists_to_mpd_format(artists): artists.sort(key=lambda a: a.name) return u', '.join([a.name for a in artists if a.name]) -def tracks_to_mpd_format(tracks, start=0, end=None, cpids=None): +def tracks_to_mpd_format(tracks, start=0, end=None): """ Format list of tracks for output to MPD client. Optionally limit output to the slice ``[start:end]`` of the list. :param tracks: the tracks - :type tracks: list of :class:`mopidy.models.Track` + :type tracks: list of :class:`mopidy.models.Track` or + :class:`mopidy.models.CpTrack` :param start: position of first track to include in output :type start: int (positive or negative) :param end: position after last track to include in output @@ -106,11 +109,10 @@ def tracks_to_mpd_format(tracks, start=0, end=None, cpids=None): end = len(tracks) tracks = tracks[start:end] positions = range(start, end) - cpids = cpids and cpids[start:end] or [None for _ in tracks] - assert len(tracks) == len(positions) == len(cpids) + assert len(tracks) == len(positions) result = [] - for track, position, cpid in zip(tracks, positions, cpids): - result.append(track_to_mpd_format(track, position, cpid)) + for track, position in zip(tracks, positions): + result.append(track_to_mpd_format(track, position)) return result def playlist_to_mpd_format(playlist, *args, **kwargs): diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 681ab20f..a20abaed 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -4,7 +4,7 @@ import os from mopidy import settings from mopidy.utils.path import mtime, uri_to_path from mopidy.frontends.mpd import translator, protocol -from mopidy.models import Album, Artist, Playlist, Track +from mopidy.models import Album, Artist, CpTrack, Playlist, Track from tests import unittest @@ -45,17 +45,17 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Pos', 1) not in result) def test_track_to_mpd_format_with_cpid(self): - result = translator.track_to_mpd_format(Track(), cpid=1) + result = translator.track_to_mpd_format(CpTrack(1, Track())) self.assert_(('Id', 1) not in result) def test_track_to_mpd_format_with_position_and_cpid(self): - result = translator.track_to_mpd_format(Track(), position=1, cpid=2) + result = translator.track_to_mpd_format(CpTrack(2, Track()), position=1) self.assert_(('Pos', 1) in result) self.assert_(('Id', 2) in result) def test_track_to_mpd_format_for_nonempty_track(self): result = translator.track_to_mpd_format( - self.track, position=9, cpid=122) + CpTrack(122, self.track), position=9) self.assert_(('file', 'a uri') in result) self.assert_(('Time', 137) in result) self.assert_(('Artist', 'an artist') in result) From ab4f21b38935b3c0e17592341c2a0cbb54e71ec7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 28 Dec 2011 00:33:42 +0100 Subject: [PATCH 10/60] Remove Track.mpd_format() which is no longer in use --- mopidy/models.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index ed323b71..24c45ff1 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -185,10 +185,6 @@ class Track(ImmutableObject): self.__dict__['artists'] = frozenset(kwargs.pop('artists', [])) super(Track, self).__init__(*args, **kwargs) - def mpd_format(self, *args, **kwargs): - from mopidy.frontends.mpd import translator - return translator.track_to_mpd_format(self, *args, **kwargs) - class Playlist(ImmutableObject): """ From 716c5b03e24ecd5cb2e7e7581e392d0964dcade2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 28 Dec 2011 00:36:51 +0100 Subject: [PATCH 11/60] Remove Playlist.mpd_format() and its usage --- mopidy/frontends/mpd/protocol/music_db.py | 9 ++++++--- mopidy/frontends/mpd/protocol/stored_playlists.py | 5 +++-- mopidy/models.py | 4 ---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 0343b3ab..299fce97 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -1,8 +1,9 @@ import re import shlex -from mopidy.frontends.mpd.protocol import handle_request, stored_playlists from mopidy.frontends.mpd.exceptions import MpdArgError, MpdNotImplemented +from mopidy.frontends.mpd.protocol import handle_request, stored_playlists +from mopidy.frontends.mpd.translator import playlist_to_mpd_format def _build_query(mpd_query): """ @@ -68,7 +69,8 @@ def find(context, mpd_query): - also uses the search type "date". """ query = _build_query(mpd_query) - return context.backend.library.find_exact(**query).get().mpd_format() + return playlist_to_mpd_format( + context.backend.library.find_exact(**query).get()) @handle_request(r'^findadd ' r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? ' @@ -324,7 +326,8 @@ def search(context, mpd_query): - also uses the search type "date". """ query = _build_query(mpd_query) - return context.backend.library.search(**query).get().mpd_format() + return playlist_to_mpd_format( + context.backend.library.search(**query).get()) @handle_request(r'^update( "(?P[^"]+)")*$') def update(context, uri=None, rescan_unmodified_files=False): diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py index 0a157f66..bb39d328 100644 --- a/mopidy/frontends/mpd/protocol/stored_playlists.py +++ b/mopidy/frontends/mpd/protocol/stored_playlists.py @@ -1,7 +1,8 @@ import datetime as dt -from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.exceptions import MpdNoExistError, MpdNotImplemented +from mopidy.frontends.mpd.protocol import handle_request +from mopidy.frontends.mpd.translator import playlist_to_mpd_format @handle_request(r'^listplaylist "(?P[^"]+)"$') def listplaylist(context, name): @@ -40,7 +41,7 @@ def listplaylistinfo(context, name): """ try: playlist = context.backend.stored_playlists.get(name=name).get() - return playlist.mpd_format() + return playlist_to_mpd_format(playlist) except LookupError: raise MpdNoExistError( u'No such playlist', command=u'listplaylistinfo') diff --git a/mopidy/models.py b/mopidy/models.py index 24c45ff1..9a508ba7 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -220,7 +220,3 @@ class Playlist(ImmutableObject): def length(self): """The number of tracks in the playlist. Read-only.""" return len(self.tracks) - - def mpd_format(self, *args, **kwargs): - from mopidy.frontends.mpd import translator - return translator.playlist_to_mpd_format(self, *args, **kwargs) From 7b0954bef8fd672f45dcf7dfd343f532fcc93368 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 28 Dec 2011 00:57:43 +0100 Subject: [PATCH 12/60] Simplify 'playlistinfo' implementation further, guided by new test asserts --- .../mpd/protocol/current_playlist.py | 19 +++++-------------- .../mpd/protocol/current_playlist_test.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index b04c86d0..ca0d1156 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -225,6 +225,7 @@ def playlistid(context, cpid=None): context.backend.current_playlist.cp_tracks.get()) @handle_request(r'^playlistinfo$') +@handle_request(r'^playlistinfo "-1"$') @handle_request(r'^playlistinfo "(?P-?\d+)"$') @handle_request(r'^playlistinfo "(?P\d+):(?P\d+)*"$') def playlistinfo(context, songpos=None, @@ -243,20 +244,10 @@ def playlistinfo(context, songpos=None, - uses negative indexes, like ``playlistinfo "-1"``, to request the entire playlist """ - if songpos == "-1": - songpos = None - if songpos is not None: songpos = int(songpos) - start = songpos - end = songpos + 1 - if start == -1: - end = None - else: - # Hot code path: Fetch a single track, while avoiding deep-copying - # the entire playlist - cp_track = context.backend.current_playlist.get(cpid=songpos).get() - return tracks_to_mpd_format([cp_track], 0, 1) + cp_track = context.backend.current_playlist.get(cpid=songpos).get() + return track_to_mpd_format(cp_track, position=songpos) else: if start is None: start = 0 @@ -267,8 +258,8 @@ def playlistinfo(context, songpos=None, end = int(end) if end > context.backend.current_playlist.length.get(): end = None - cp_tracks = context.backend.current_playlist.cp_tracks.get() - return tracks_to_mpd_format(cp_tracks, start, end) + cp_tracks = context.backend.current_playlist.cp_tracks.get() + return tracks_to_mpd_format(cp_tracks, start, end) @handle_request(r'^playlistsearch "(?P[^"]+)" "(?P[^"]+)"$') @handle_request(r'^playlistsearch (?P\S+) "(?P[^"]+)"$') diff --git a/tests/frontends/mpd/protocol/current_playlist_test.py b/tests/frontends/mpd/protocol/current_playlist_test.py index 343b230b..321fc6ee 100644 --- a/tests/frontends/mpd/protocol/current_playlist_test.py +++ b/tests/frontends/mpd/protocol/current_playlist_test.py @@ -271,11 +271,17 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.sendRequest(u'playlistinfo') self.assertInResponse(u'Title: a') + self.assertInResponse(u'Pos: 0') self.assertInResponse(u'Title: b') + self.assertInResponse(u'Pos: 1') self.assertInResponse(u'Title: c') + self.assertInResponse(u'Pos: 2') self.assertInResponse(u'Title: d') + self.assertInResponse(u'Pos: 3') self.assertInResponse(u'Title: e') + self.assertInResponse(u'Pos: 4') self.assertInResponse(u'Title: f') + self.assertInResponse(u'Pos: 5') self.assertInResponse(u'OK') def test_playlistinfo_with_songpos(self): @@ -286,11 +292,17 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.sendRequest(u'playlistinfo "4"') self.assertNotInResponse(u'Title: a') + self.assertNotInResponse(u'Pos: 0') self.assertNotInResponse(u'Title: b') + self.assertNotInResponse(u'Pos: 1') self.assertNotInResponse(u'Title: c') + self.assertNotInResponse(u'Pos: 2') self.assertNotInResponse(u'Title: d') + self.assertNotInResponse(u'Pos: 3') self.assertInResponse(u'Title: e') + self.assertInResponse(u'Pos: 4') self.assertNotInResponse(u'Title: f') + self.assertNotInResponse(u'Pos: 5') self.assertInResponse(u'OK') def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self): @@ -306,11 +318,17 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.sendRequest(u'playlistinfo "2:"') self.assertNotInResponse(u'Title: a') + self.assertNotInResponse(u'Pos: 0') self.assertNotInResponse(u'Title: b') + self.assertNotInResponse(u'Pos: 1') self.assertInResponse(u'Title: c') + self.assertInResponse(u'Pos: 2') self.assertInResponse(u'Title: d') + self.assertInResponse(u'Pos: 3') self.assertInResponse(u'Title: e') + self.assertInResponse(u'Pos: 4') self.assertInResponse(u'Title: f') + self.assertInResponse(u'Pos: 5') self.assertInResponse(u'OK') def test_playlistinfo_with_closed_range(self): From cda2fbbe965765a1cdf4224fb125b2c70bbe4d66 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 28 Dec 2011 01:55:02 +0100 Subject: [PATCH 13/60] Add index() method to CurrentPlaylistController to reduce copying of the playlist --- docs/changes.rst | 7 ++++--- mopidy/backends/base/current_playlist.py | 14 +++++++++++++- mopidy/frontends/mpd/protocol/current_playlist.py | 14 +++++--------- tests/backends/base/current_playlist.py | 14 +++++++++++++- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index ac2c21bf..5bebd49e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -18,13 +18,14 @@ v0.7.0 (in development) if you use playlist folders, you will no longer get lots of log messages about bad playlists. +- The MPD command ``playlistinfo`` is now faster, thanks to John Bäckstrand. + - Added the method - :meth:`mopidy.backends.base.CurrentPlaylistController.length()` to reduce the + :meth:`mopidy.backends.base.CurrentPlaylistController.length()` and + :meth:`mopidy.backends.base.CurrentPlaylistController.index()` to reduce the need for copying the entire current playlist from one thread to another. Thanks to John Bäckstrand. -- The MPD command ``playlistinfo`` is now faster, thanks to John Bäckstrand. - v0.6.0 (2011-10-09) =================== diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index 966456a4..9764ed6a 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -136,6 +136,19 @@ class CurrentPlaylistController(object): else: raise LookupError(u'"%s" match multiple tracks' % criteria_string) + def index(self, cp_track): + """ + Get index of the given (CPID integer, :class:`mopidy.models.Track`) + two-tuple in the current playlist. + + Raises :exc:`ValueError` if not found. + + :param cp_track: track to find the index of + :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`) + :rtype: int + """ + return self._cp_tracks.index(cp_track) + def move(self, start, end, to_position): """ Move the tracks in the slice ``[start:end]`` to ``to_position``. @@ -175,7 +188,6 @@ class CurrentPlaylistController(object): :param criteria: on or more criteria to match by :type criteria: dict - :type track: :class:`mopidy.models.Track` """ cp_track = self.get(**criteria) position = self._cp_tracks.index(cp_track) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index ca0d1156..9d8c73b2 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -158,8 +158,7 @@ def moveid(context, cpid, to): cpid = int(cpid) to = int(to) cp_track = context.backend.current_playlist.get(cpid=cpid).get() - position = context.backend.current_playlist.cp_tracks.get().index( - cp_track) + position = context.backend.current_playlist.index(cp_track).get() context.backend.current_playlist.move(position, position + 1, to) @handle_request(r'^playlist$') @@ -194,8 +193,7 @@ def playlistfind(context, tag, needle): if tag == 'filename': try: cp_track = context.backend.current_playlist.get(uri=needle).get() - position = context.backend.current_playlist.cp_tracks.get().index( - cp_track) + position = context.backend.current_playlist.index(cp_track).get() return track_to_mpd_format(cp_track, position=position) except LookupError: return None @@ -215,8 +213,7 @@ def playlistid(context, cpid=None): try: cpid = int(cpid) cp_track = context.backend.current_playlist.get(cpid=cpid).get() - position = context.backend.current_playlist.cp_tracks.get().index( - cp_track) + position = context.backend.current_playlist.index(cp_track).get() return track_to_mpd_format(cp_track, position=position) except LookupError: raise MpdNoExistError(u'No such song', command=u'playlistid') @@ -375,7 +372,6 @@ def swapid(context, cpid1, cpid2): cpid2 = int(cpid2) cp_track1 = context.backend.current_playlist.get(cpid=cpid1).get() cp_track2 = context.backend.current_playlist.get(cpid=cpid2).get() - cp_tracks = context.backend.current_playlist.cp_tracks.get() - position1 = cp_tracks.index(cp_track1) - position2 = cp_tracks.index(cp_track2) + position1 = context.backend.current_playlist.index(cp_track1).get() + position2 = context.backend.current_playlist.index(cp_track2).get() swap(context, position1, position2) diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py index 6d4854a7..2dab625e 100644 --- a/tests/backends/base/current_playlist.py +++ b/tests/backends/base/current_playlist.py @@ -1,7 +1,7 @@ import mock import random -from mopidy.models import Playlist, Track +from mopidy.models import CpTrack, Playlist, Track from mopidy.gstreamer import GStreamer from tests.backends.base import populate_playlist @@ -143,6 +143,18 @@ class CurrentPlaylistControllerTest(object): self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.current_track, None) + def test_index_returns_index_of_track(self): + cp_tracks = [] + for track in self.tracks: + cp_tracks.append(self.controller.add(track)) + self.assertEquals(0, self.controller.index(cp_tracks[0])) + self.assertEquals(1, self.controller.index(cp_tracks[1])) + self.assertEquals(2, self.controller.index(cp_tracks[2])) + + def test_index_raises_value_error_if_item_not_found(self): + test = lambda: self.controller.index(CpTrack(0, Track())) + self.assertRaises(ValueError, test) + @populate_playlist def test_move_single(self): self.controller.move(0, 0, 2) From 5573c33a7059f156b076b877f92e17292ae30075 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 28 Dec 2011 01:57:47 +0100 Subject: [PATCH 14/60] Replace CpTrack indexes with namedtuple field names --- mopidy/backends/base/current_playlist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index 9764ed6a..4e439931 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -28,7 +28,7 @@ class CurrentPlaylistController(object): Read-only. """ - return [copy(ct) for ct in self._cp_tracks] + return [copy(cp_track) for cp_track in self._cp_tracks] @property def tracks(self): @@ -37,7 +37,7 @@ class CurrentPlaylistController(object): Read-only. """ - return [ct[1] for ct in self._cp_tracks] + return [cp_track.track for cp_track in self._cp_tracks] @property def length(self): @@ -123,9 +123,9 @@ class CurrentPlaylistController(object): matches = self._cp_tracks for (key, value) in criteria.iteritems(): if key == 'cpid': - matches = filter(lambda ct: ct[0] == value, matches) + matches = filter(lambda ct: ct.cpid == value, matches) else: - matches = filter(lambda ct: getattr(ct[1], key) == value, + matches = filter(lambda ct: getattr(ct.track, key) == value, matches) if len(matches) == 1: return matches[0] From 4f8fbac44c1636e54c08060e51980a1b138857f8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 28 Dec 2011 02:03:19 +0100 Subject: [PATCH 15/60] Use CurrentPlaylistController.length once more --- mopidy/frontends/mpd/protocol/current_playlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 9d8c73b2..91ef7811 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -75,7 +75,7 @@ def delete_range(context, start, end=None): if end is not None: end = int(end) else: - end = len(context.backend.current_playlist.tracks.get()) + end = context.backend.current_playlist.length.get() cp_tracks = context.backend.current_playlist.cp_tracks.get()[start:end] if not cp_tracks: raise MpdArgError(u'Bad song index', command=u'delete') From b0698d2e0ad306bbf1bd5259d9b8abd547d7eb29 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 28 Dec 2011 02:38:07 +0100 Subject: [PATCH 16/60] Add slice() method to CurrentPlaylistController to reduce copying of the playlist --- docs/changes.rst | 7 ++++--- mopidy/backends/base/current_playlist.py | 13 +++++++++++++ mopidy/frontends/mpd/protocol/current_playlist.py | 5 +++-- mopidy/frontends/mpd/protocol/playback.py | 7 ++++--- tests/backends/base/current_playlist.py | 12 ++++++++++++ 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 5bebd49e..95541797 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -21,10 +21,11 @@ v0.7.0 (in development) - The MPD command ``playlistinfo`` is now faster, thanks to John Bäckstrand. - Added the method - :meth:`mopidy.backends.base.CurrentPlaylistController.length()` and - :meth:`mopidy.backends.base.CurrentPlaylistController.index()` to reduce the + :meth:`mopidy.backends.base.CurrentPlaylistController.length()`, + :meth:`mopidy.backends.base.CurrentPlaylistController.index()`, and + :meth:`mopidy.backends.base.CurrentPlaylistController.slice()` to reduce the need for copying the entire current playlist from one thread to another. - Thanks to John Bäckstrand. + Thanks to John Bäckstrand for pinpointing the issue. v0.6.0 (2011-10-09) diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index 4e439931..d7e6c331 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -223,6 +223,19 @@ class CurrentPlaylistController(object): self._cp_tracks = before + shuffled + after self.version += 1 + def slice(self, start, end): + """ + Returns a slice of the current playlist, limited by the given + start and end positions. + + :param start: position of first track to include in slice + :type start: int + :param end: position after last track to include in slice + :type end: int + :rtype: two-tuple of (CPID integer, :class:`mopidy.models.Track`) + """ + return [copy(cp_track) for cp_track in self._cp_tracks[start:end]] + def _trigger_playlist_changed(self): logger.debug(u'Triggering playlist changed event') BackendListener.send('playlist_changed') diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 91ef7811..0d61c887 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -76,7 +76,7 @@ def delete_range(context, start, end=None): end = int(end) else: end = context.backend.current_playlist.length.get() - cp_tracks = context.backend.current_playlist.cp_tracks.get()[start:end] + cp_tracks = context.backend.current_playlist.slice(start, end).get() if not cp_tracks: raise MpdArgError(u'Bad song index', command=u'delete') for (cpid, _) in cp_tracks: @@ -87,7 +87,8 @@ def delete_songpos(context, songpos): """See :meth:`delete_range`""" try: songpos = int(songpos) - (cpid, _) = context.backend.current_playlist.cp_tracks.get()[songpos] + (cpid, _) = context.backend.current_playlist.slice( + songpos, songpos + 1).get()[0] context.backend.current_playlist.remove(cpid=cpid) except IndexError: raise MpdArgError(u'Bad song index', command=u'delete') diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 63cfe649..948083a8 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -178,7 +178,8 @@ def playpos(context, songpos): if songpos == -1: return _play_minus_one(context) try: - cp_track = context.backend.current_playlist.cp_tracks.get()[songpos] + cp_track = context.backend.current_playlist.slice( + songpos, songpos + 1).get()[0] return context.backend.playback.play(cp_track).get() except IndexError: raise MpdArgError(u'Bad song index', command=u'play') @@ -191,8 +192,8 @@ def _play_minus_one(context): elif context.backend.playback.current_cp_track.get() is not None: cp_track = context.backend.playback.current_cp_track.get() return context.backend.playback.play(cp_track).get() - elif context.backend.current_playlist.cp_tracks.get(): - cp_track = context.backend.current_playlist.cp_tracks.get()[0] + elif context.backend.current_playlist.slice(0, 1).get(): + cp_track = context.backend.current_playlist.slice(0, 1).get()[0] return context.backend.playback.play(cp_track).get() else: return # Fail silently diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py index 2dab625e..e99cd56c 100644 --- a/tests/backends/base/current_playlist.py +++ b/tests/backends/base/current_playlist.py @@ -260,6 +260,18 @@ class CurrentPlaylistControllerTest(object): self.assertEqual(self.tracks[0], shuffled_tracks[0]) self.assertEqual(set(self.tracks), set(shuffled_tracks)) + @populate_playlist + def test_slice_returns_a_subset_of_tracks(self): + track_slice = self.controller.slice(1, 3) + self.assertEqual(2, len(track_slice)) + self.assertEqual(self.tracks[1], track_slice[0].track) + self.assertEqual(self.tracks[2], track_slice[1].track) + + @populate_playlist + def test_slice_returns_empty_list_if_indexes_outside_tracks_list(self): + self.assertEqual(0, len(self.controller.slice(7, 8))) + self.assertEqual(0, len(self.controller.slice(-1, 1))) + def test_version_does_not_change_when_appending_nothing(self): version = self.controller.version self.controller.append([]) From 088f3e8505cb2f218851f61fc4bbb95dde908b6c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 28 Dec 2011 23:56:52 +0100 Subject: [PATCH 17/60] Check if album is loaded before use --- mopidy/backends/spotify/translator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index cc72aecd..27f4719b 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -31,9 +31,10 @@ class SpotifyTranslator(object): uri = str(Link.from_track(spotify_track, 0)) if not spotify_track.is_loaded(): return Track(uri=uri, name=u'[loading...]') - if (spotify_track.album() is not None and - dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR): - date = dt.date(spotify_track.album().year(), 1, 1) + spotify_album = spotify_track.album() + if (spotify_album is not None and spotify_album.is_loaded() + and dt.MINYEAR <= int(spotify_album.year()) <= dt.MAXYEAR): + date = dt.date(spotify_album.year(), 1, 1) else: date = None return Track( From dd5336539dd74721fe1b636ea24c4eb0f58774ab Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Jan 2012 22:20:41 +0100 Subject: [PATCH 18/60] Make sure the calls returns strings even when the object we call on is an mock --- mopidy/__init__.py | 6 +++--- mopidy/backends/local/__init__.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index ced47e07..b94378b2 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -10,9 +10,9 @@ from subprocess import PIPE, Popen VERSION = (0, 7, 0) -DATA_PATH = os.path.join(glib.get_user_data_dir(), 'mopidy') -CACHE_PATH = os.path.join(glib.get_user_cache_dir(), 'mopidy') -SETTINGS_PATH = os.path.join(glib.get_user_config_dir(), 'mopidy') +DATA_PATH = os.path.join(str(glib.get_user_data_dir()), 'mopidy') +CACHE_PATH = os.path.join(str(glib.get_user_cache_dir()), 'mopidy') +SETTINGS_PATH = os.path.join(str(glib.get_user_config_dir()), 'mopidy') SETTINGS_FILE = os.path.join(SETTINGS_PATH, 'settings.py') def get_version(): diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index e1d11bcb..e8638a3a 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -21,7 +21,7 @@ logger = logging.getLogger(u'mopidy.backends.local') DEFAULT_PLAYLIST_PATH = os.path.join(DATA_PATH, 'playlists') DEFAULT_TAG_CACHE_FILE = os.path.join(DATA_PATH, 'tag_cache') -DEFAULT_MUSIC_PATH = glib.get_user_special_dir(glib.USER_DIRECTORY_MUSIC) +DEFAULT_MUSIC_PATH = str(glib.get_user_special_dir(glib.USER_DIRECTORY_MUSIC)) if not DEFAULT_MUSIC_PATH or DEFAULT_MUSIC_PATH == os.path.expanduser(u'~'): DEFAULT_MUSIC_PATH = os.path.expanduser(u'~/music') From 1952f412d5b8ad713e163e454ecebe0c69ce17ec Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Jan 2012 22:21:26 +0100 Subject: [PATCH 19/60] Don't setup DBusGMainLoop on module import --- mopidy/frontends/mpris/objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index 77278778..e0a83e03 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -23,7 +23,6 @@ from mopidy.utils.process import exit_process # Must be done before dbus.SessionBus() is called gobject.threads_init() dbus.mainloop.glib.threads_init() -dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) BUS_NAME = 'org.mpris.MediaPlayer2.mopidy' OBJECT_PATH = '/org/mpris/MediaPlayer2' @@ -81,7 +80,8 @@ class MprisObject(dbus.service.Object): def _connect_to_dbus(self): logger.debug(u'Connecting to D-Bus...') - bus_name = dbus.service.BusName(BUS_NAME, dbus.SessionBus()) + mainloop = dbus.mainloop.glib.DBusGMainLoop() + bus_name = dbus.service.BusName(BUS_NAME, dbus.SessionBus(mainloop=mainloop)) logger.info(u'Connected to D-Bus') return bus_name From 76c544833e70f6a48dc462e73a950b7aaea04e2d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Jan 2012 22:21:51 +0100 Subject: [PATCH 20/60] Don't create Caps object on module import, as gst.Caps may be a mock --- mopidy/gstreamer.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index edcb3084..ffb8c4f1 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -13,15 +13,6 @@ from mopidy.backends.base import Backend logger = logging.getLogger('mopidy.gstreamer') -default_caps = gst.Caps(""" - audio/x-raw-int, - endianness=(int)1234, - channels=(int)2, - width=(int)16, - depth=(int)16, - signed=(boolean)true, - rate=(int)44100""") - class GStreamer(ThreadingActor): """ @@ -34,6 +25,14 @@ class GStreamer(ThreadingActor): """ def __init__(self): + self._default_caps = gst.Caps(""" + audio/x-raw-int, + endianness=(int)1234, + channels=(int)2, + width=(int)16, + depth=(int)16, + signed=(boolean)true, + rate=(int)44100""") self._pipeline = None self._source = None self._tee = None @@ -77,7 +76,7 @@ class GStreamer(ThreadingActor): def _on_new_source(self, element, pad): self._source = element.get_property('source') try: - self._source.set_property('caps', default_caps) + self._source.set_property('caps', self._default_caps) except TypeError: pass From 6516d0726e7c75e24051e97883bab98a0c98e44f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Jan 2012 22:22:45 +0100 Subject: [PATCH 21/60] Mock all external dependencies when building docs --- docs/conf.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index aeada340..9a1c7db6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,6 +13,42 @@ import sys, os +class Mock(object): + def __init__(self, *args, **kwargs): + pass + + def __call__(self, *args, **kwargs): + return Mock() + + @classmethod + def __getattr__(self, name): + if name in ('__file__', '__path__'): + return '/dev/null' + elif name[0] == name[0].upper(): + return type(name, (), {}) + else: + return Mock() + +MOCK_MODULES = [ + 'alsaaudio', + 'dbus', + 'dbus.mainloop', + 'dbus.mainloop.glib', + 'dbus.service', + 'glib', + 'gobject', + 'gst', + 'pygst', + 'pykka', + 'pykka.actor', + 'pykka.future', + 'pykka.registry', + 'pylast', + 'serial', +] +for mod_name in MOCK_MODULES: + sys.modules[mod_name] = Mock() + # 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. From 57f1354c0aab48d9db9e41f8994e525eb071a512 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Jan 2012 22:23:56 +0100 Subject: [PATCH 22/60] Remove the inheritance diagrams from the docs, as they don't work with mocked out dependencies --- docs/conf.py | 3 +-- docs/modules/frontends/mpd.rst | 4 ---- docs/modules/gstreamer.rst | 2 -- docs/modules/mixers/alsa.rst | 2 -- docs/modules/mixers/denon.rst | 2 -- docs/modules/mixers/dummy.rst | 2 -- docs/modules/mixers/gstreamer_software.rst | 2 -- docs/modules/mixers/nad.rst | 2 -- docs/modules/mixers/osa.rst | 2 -- docs/modules/outputs.rst | 3 --- 10 files changed, 1 insertion(+), 23 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9a1c7db6..7d15cd6b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,8 +61,7 @@ import mopidy # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.graphviz', 'sphinx.ext.inheritance_diagram', +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.graphviz', 'sphinx.ext.extlinks', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/modules/frontends/mpd.rst b/docs/modules/frontends/mpd.rst index b0c7e3c5..0ce138a2 100644 --- a/docs/modules/frontends/mpd.rst +++ b/docs/modules/frontends/mpd.rst @@ -2,8 +2,6 @@ :mod:`mopidy.frontends.mpd` -- MPD server ***************************************** -.. inheritance-diagram:: mopidy.frontends.mpd - .. automodule:: mopidy.frontends.mpd :synopsis: MPD server frontend :members: @@ -12,8 +10,6 @@ MPD dispatcher ============== -.. inheritance-diagram:: mopidy.frontends.mpd.dispatcher - .. automodule:: mopidy.frontends.mpd.dispatcher :synopsis: MPD request dispatcher :members: diff --git a/docs/modules/gstreamer.rst b/docs/modules/gstreamer.rst index adbf5fda..205b0a3e 100644 --- a/docs/modules/gstreamer.rst +++ b/docs/modules/gstreamer.rst @@ -2,8 +2,6 @@ :mod:`mopidy.gstreamer` -- GStreamer adapter ******************************************** -.. inheritance-diagram:: mopidy.gstreamer - .. automodule:: mopidy.gstreamer :synopsis: GStreamer adapter :members: diff --git a/docs/modules/mixers/alsa.rst b/docs/modules/mixers/alsa.rst index 05f429eb..e8b7ed6c 100644 --- a/docs/modules/mixers/alsa.rst +++ b/docs/modules/mixers/alsa.rst @@ -2,8 +2,6 @@ :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 index ac944ccc..7fb2d6cc 100644 --- a/docs/modules/mixers/denon.rst +++ b/docs/modules/mixers/denon.rst @@ -2,8 +2,6 @@ :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 index 6665f949..8ac18e10 100644 --- a/docs/modules/mixers/dummy.rst +++ b/docs/modules/mixers/dummy.rst @@ -2,8 +2,6 @@ :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 index ef8cc310..98e09f44 100644 --- a/docs/modules/mixers/gstreamer_software.rst +++ b/docs/modules/mixers/gstreamer_software.rst @@ -2,8 +2,6 @@ :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 index d441b3fd..56291cbb 100644 --- a/docs/modules/mixers/nad.rst +++ b/docs/modules/mixers/nad.rst @@ -2,8 +2,6 @@ :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 index 14bf9a49..a4363cb4 100644 --- a/docs/modules/mixers/osa.rst +++ b/docs/modules/mixers/osa.rst @@ -2,8 +2,6 @@ :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.rst b/docs/modules/outputs.rst index 87d23dab..f80c16e3 100644 --- a/docs/modules/outputs.rst +++ b/docs/modules/outputs.rst @@ -4,11 +4,8 @@ The following GStreamer audio outputs implements the :ref:`output-api`. -.. inheritance-diagram:: mopidy.outputs.custom .. autoclass:: mopidy.outputs.custom.CustomOutput -.. inheritance-diagram:: mopidy.outputs.local .. autoclass:: mopidy.outputs.local.LocalOutput -.. inheritance-diagram:: mopidy.outputs.shoutcast .. autoclass:: mopidy.outputs.shoutcast.ShoutcastOutput From 5bf93cb0e0e67dd592f157276d4270523f9429b0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Jan 2012 22:24:46 +0100 Subject: [PATCH 23/60] Update copyright --- 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 7d15cd6b..3a18950d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -78,7 +78,7 @@ master_doc = 'index' # General information about the project. project = u'Mopidy' -copyright = u'2010-2011, Stein Magnus Jodal and contributors' +copyright = u'2010-2012, 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 7f4ed0ce..11e0a906 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-2011 Stein Magnus Jodal and contributors +Copyright 2009-2012 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-2011 Stein Magnus Jodal and contributors +Copyright 2010-2012 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 906402ac47c8b6401c39dc46868904e6c073eebf Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Jan 2012 22:27:16 +0100 Subject: [PATCH 24/60] Use default theme on Read The Docs --- docs/conf.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 3a18950d..c05b13f7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,7 +55,9 @@ for mod_name in MOCK_MODULES: sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/../')) -import mopidy +# When RTD builds the project, it sets the READTHEDOCS environment variable to +# the string True. +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' # -- General configuration ----------------------------------------------------- @@ -85,6 +87,7 @@ copyright = u'2010-2012, Stein Magnus Jodal and contributors' # built documents. # # The full version, including alpha/beta/rc tags. +import mopidy release = mopidy.get_version() # The short X.Y version. version = '.'.join(release.split('.')[:2]) @@ -132,7 +135,10 @@ modindex_common_prefix = ['mopidy.'] # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'nature' +if on_rtd: + html_theme = 'default' +else: + html_theme = 'nature' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From 315661ec19141e12a9d7abe18659aa236caab830 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Jan 2012 22:43:00 +0100 Subject: [PATCH 25/60] Point documentation links at docs.mopidy.com --- README.rst | 6 ++---- docs/development/contributing.rst | 14 ++------------ docs/index.rst | 4 +--- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index c063de79..7415c670 100644 --- a/README.rst +++ b/README.rst @@ -9,11 +9,9 @@ in Spotify's vast archive, manage playlists, and play music, you can use most platforms, including Windows, Mac OS X, Linux, Android and iOS. To install Mopidy, check out -`the installation docs `_. +`the installation docs `_. -- `Documentation for the latest release `_ -- `Documentation for the development version - `_ +- `Documentation for the latest release `_ - `Source code `_ - `Issue tracker `_ - IRC: ``#mopidy`` at `irc.freenode.net `_ diff --git a/docs/development/contributing.rst b/docs/development/contributing.rst index 9ea3533f..0303bdc7 100644 --- a/docs/development/contributing.rst +++ b/docs/development/contributing.rst @@ -134,18 +134,8 @@ Then, to generate docs:: make # For help on available targets make html # To generate HTML docs -.. note:: - - The documentation at http://www.mopidy.com/ is automatically updated when a - documentation update is pushed to ``mopidy/mopidy`` at GitHub. - - Documentation generated from the ``master`` branch is published at - http://www.mopidy.com/docs/master/, and will always be valid for the latest - release. - - Documentation generated from the ``develop`` branch is published at - http://www.mopidy.com/docs/develop/, and will always be valid for the - latest development snapshot. +The documentation at http://docs.mopidy.com/ is automatically updated when a +documentation update is pushed to ``mopidy/mopidy`` at GitHub. Creating releases diff --git a/docs/index.rst b/docs/index.rst index 769aed20..7e757de0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,9 +19,7 @@ please create an issue in the `issue tracker Project resources ================= -- `Documentation for the latest release `_ -- `Documentation for the development version - `_ +- `Documentation `_ - `Source code `_ - `Issue tracker `_ - IRC: ``#mopidy`` at `irc.freenode.net `_ From 55f5846238fe8e960b4f8bef9bfee22de736fc8f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Jan 2012 23:11:41 +0100 Subject: [PATCH 26/60] The link is for all docs, not just the latest release --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7415c670..13ab0f92 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ platforms, including Windows, Mac OS X, Linux, Android and iOS. To install Mopidy, check out `the installation docs `_. -- `Documentation for the latest release `_ +- `Documentation `_ - `Source code `_ - `Issue tracker `_ - IRC: ``#mopidy`` at `irc.freenode.net `_ From 3af3c3371f2384aa6420c986295bc53679550ac7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Jan 2012 23:36:19 +0100 Subject: [PATCH 27/60] Remove last generated date in docs footer --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index c05b13f7..d14075d0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -171,7 +171,7 @@ html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -html_last_updated_fmt = '%b %d, %Y' +#html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. From 9a17ccd1e3e6256aa73a598d1c55a929fd0fec58 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Jan 2012 00:57:52 +0100 Subject: [PATCH 28/60] Update links to Pykka web site --- docs/api/frontends.rst | 2 +- docs/changes.rst | 2 +- docs/installation/index.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/frontends.rst b/docs/api/frontends.rst index dc53cca2..af0cc991 100644 --- a/docs/api/frontends.rst +++ b/docs/api/frontends.rst @@ -7,7 +7,7 @@ The following requirements applies to any frontend implementation: - A frontend MAY do mostly whatever it wants to, including creating threads, opening TCP ports and exposing Mopidy for a group of clients. - A frontend MUST implement at least one `Pykka - `_ actor, called the "main actor" from here + `_ actor, called the "main actor" from here on. - It MAY use additional actors to implement whatever it does, and using actors in frontend implementations is encouraged. diff --git a/docs/changes.rst b/docs/changes.rst index dfc777f0..8235e95d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -261,7 +261,7 @@ loading from Mopidy 0.3.0 is still present. **Important changes** -- Mopidy now depends on `Pykka `_ >=0.12. If you +- Mopidy now depends on `Pykka `_ >=0.12. If you install from APT, Pykka will automatically be installed. If you are not installing from APT, you may install Pykka from PyPI:: diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 8fd3e840..8dd47ada 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -25,7 +25,7 @@ Otherwise, make sure you got the required dependencies installed. - Python >= 2.6, < 3 -- `Pykka `_ >= 0.12.3 +- `Pykka `_ >= 0.12.3 - GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`. From 8d64ee8f5404597139dc21111a84b908b579a65f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 17 Jan 2012 18:19:52 +0100 Subject: [PATCH 29/60] Remove custom Sphinx theme and template not needed when using Read The Docs --- docs/_templates/layout.html | 15 -- docs/_themes/nature/static/nature.css_t | 236 ------------------------ docs/_themes/nature/static/pygments.css | 54 ------ docs/_themes/nature/theme.conf | 4 - docs/conf.py | 8 +- 5 files changed, 3 insertions(+), 314 deletions(-) delete mode 100644 docs/_templates/layout.html delete mode 100644 docs/_themes/nature/static/nature.css_t delete mode 100644 docs/_themes/nature/static/pygments.css delete mode 100644 docs/_themes/nature/theme.conf diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html deleted file mode 100644 index 485debc5..00000000 --- a/docs/_templates/layout.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "!layout.html" %} - -{% block extrahead %} -{{ super() }} - -{% endblock %} diff --git a/docs/_themes/nature/static/nature.css_t b/docs/_themes/nature/static/nature.css_t deleted file mode 100644 index b6c0f22e..00000000 --- a/docs/_themes/nature/static/nature.css_t +++ /dev/null @@ -1,236 +0,0 @@ -/** - * Sphinx stylesheet -- default theme - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: Arial, sans-serif; - font-size: 100%; - background-color: #111111; - color: #555555; - margin: 0; - padding: 0; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 300px; -} - -hr{ - border: 1px solid #B1B4B6; -} - -div.document { - background-color: #eeeeee; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 1em 30px 30px 30px; - font-size: 0.9em; -} - -div.footer { - color: #555; - width: 100%; - padding: 13px 0; - text-align: center; - font-size: 75%; -} - -div.footer a { - color: #444444; -} - -div.related { - background-color: #6BA81E; - line-height: 36px; - color: #ffffff; - text-shadow: 0px 1px 0 #444444; - font-size: 1.1em; -} - -div.related a { - color: #E2F3CC; -} - -div.related .right { - font-size: 0.9em; -} - -div.sphinxsidebar { - font-size: 0.9em; - line-height: 1.5em; - width: 300px -} - -div.sphinxsidebarwrapper{ - padding: 20px 0; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: Arial, sans-serif; - color: #222222; - font-size: 1.2em; - font-weight: bold; - margin: 0; - padding: 5px 10px; - text-shadow: 1px 1px 0 white -} - -div.sphinxsidebar h3 a { - color: #444444; -} - -div.sphinxsidebar p { - color: #888888; - padding: 5px 20px; - margin: 0.5em 0px; -} - -div.sphinxsidebar p.topless { -} - -div.sphinxsidebar ul { - margin: 10px 10px 10px 20px; - padding: 0; - color: #000000; -} - -div.sphinxsidebar a { - color: #444444; -} - -div.sphinxsidebar a:hover { - color: #E32E00; -} - -div.sphinxsidebar input { - border: 1px solid #cccccc; - font-family: sans-serif; - font-size: 1.1em; - padding: 0.15em 0.3em; -} - -div.sphinxsidebar input[type=text]{ - margin-left: 20px; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #005B81; - text-decoration: none; -} - -a:hover { - color: #E32E00; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: Arial, sans-serif; - font-weight: normal; - color: #212224; - margin: 30px 0px 10px 0px; - padding: 5px 0 5px 0px; - text-shadow: 0px 1px 0 white; - border-bottom: 1px solid #C8D5E3; -} - -div.body h1 { margin-top: 0; font-size: 200%; } -div.body h2 { font-size: 150%; } -div.body h3 { font-size: 120%; } -div.body h4 { font-size: 110%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #c60f0f; - font-size: 0.8em; - padding: 0 4px 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - background-color: #c60f0f; - color: white; -} - -div.body p, div.body dd, div.body li { - line-height: 1.8em; -} - -div.admonition p.admonition-title + p { - display: inline; -} - -div.highlight{ - background-color: white; -} - -div.note { - background-color: #eeeeee; - border: 1px solid #cccccc; -} - -div.seealso { - background-color: #ffffcc; - border: 1px solid #ffff66; -} - -div.topic { - background-color: #fafafa; - border-width: 0; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #ff6666; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre { - padding: 10px; - background-color: #eeeeee; - color: #222222; - line-height: 1.5em; - font-size: 1.1em; - margin: 1.5em 0 1.5em 0; - -webkit-box-shadow: 0px 0px 4px #d8d8d8; - -moz-box-shadow: 0px 0px 4px #d8d8d8; - box-shadow: 0px 0px 4px #d8d8d8; -} - -tt { - color: #222222; - padding: 1px 2px; - font-size: 1.2em; - font-family: monospace; -} - -#table-of-contents ul { - padding-left: 2em; -} diff --git a/docs/_themes/nature/static/pygments.css b/docs/_themes/nature/static/pygments.css deleted file mode 100644 index 652b7612..00000000 --- a/docs/_themes/nature/static/pygments.css +++ /dev/null @@ -1,54 +0,0 @@ -.c { color: #999988; font-style: italic } /* Comment */ -.k { font-weight: bold } /* Keyword */ -.o { font-weight: bold } /* Operator */ -.cm { color: #999988; font-style: italic } /* Comment.Multiline */ -.cp { color: #999999; font-weight: bold } /* Comment.preproc */ -.c1 { color: #999988; font-style: italic } /* Comment.Single */ -.gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ -.ge { font-style: italic } /* Generic.Emph */ -.gr { color: #aa0000 } /* Generic.Error */ -.gh { color: #999999 } /* Generic.Heading */ -.gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ -.go { color: #111 } /* Generic.Output */ -.gp { color: #555555 } /* Generic.Prompt */ -.gs { font-weight: bold } /* Generic.Strong */ -.gu { color: #aaaaaa } /* Generic.Subheading */ -.gt { color: #aa0000 } /* Generic.Traceback */ -.kc { font-weight: bold } /* Keyword.Constant */ -.kd { font-weight: bold } /* Keyword.Declaration */ -.kp { font-weight: bold } /* Keyword.Pseudo */ -.kr { font-weight: bold } /* Keyword.Reserved */ -.kt { color: #445588; font-weight: bold } /* Keyword.Type */ -.m { color: #009999 } /* Literal.Number */ -.s { color: #bb8844 } /* Literal.String */ -.na { color: #008080 } /* Name.Attribute */ -.nb { color: #999999 } /* Name.Builtin */ -.nc { color: #445588; font-weight: bold } /* Name.Class */ -.no { color: #ff99ff } /* Name.Constant */ -.ni { color: #800080 } /* Name.Entity */ -.ne { color: #990000; font-weight: bold } /* Name.Exception */ -.nf { color: #990000; font-weight: bold } /* Name.Function */ -.nn { color: #555555 } /* Name.Namespace */ -.nt { color: #000080 } /* Name.Tag */ -.nv { color: purple } /* Name.Variable */ -.ow { font-weight: bold } /* Operator.Word */ -.mf { color: #009999 } /* Literal.Number.Float */ -.mh { color: #009999 } /* Literal.Number.Hex */ -.mi { color: #009999 } /* Literal.Number.Integer */ -.mo { color: #009999 } /* Literal.Number.Oct */ -.sb { color: #bb8844 } /* Literal.String.Backtick */ -.sc { color: #bb8844 } /* Literal.String.Char */ -.sd { color: #bb8844 } /* Literal.String.Doc */ -.s2 { color: #bb8844 } /* Literal.String.Double */ -.se { color: #bb8844 } /* Literal.String.Escape */ -.sh { color: #bb8844 } /* Literal.String.Heredoc */ -.si { color: #bb8844 } /* Literal.String.Interpol */ -.sx { color: #bb8844 } /* Literal.String.Other */ -.sr { color: #808000 } /* Literal.String.Regex */ -.s1 { color: #bb8844 } /* Literal.String.Single */ -.ss { color: #bb8844 } /* Literal.String.Symbol */ -.bp { color: #999999 } /* Name.Builtin.Pseudo */ -.vc { color: #ff99ff } /* Name.Variable.Class */ -.vg { color: #ff99ff } /* Name.Variable.Global */ -.vi { color: #ff99ff } /* Name.Variable.Instance */ -.il { color: #009999 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_themes/nature/theme.conf b/docs/_themes/nature/theme.conf deleted file mode 100644 index 1cc40044..00000000 --- a/docs/_themes/nature/theme.conf +++ /dev/null @@ -1,4 +0,0 @@ -[theme] -inherit = basic -stylesheet = nature.css -pygments_style = tango diff --git a/docs/conf.py b/docs/conf.py index d14075d0..b4dbc35b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -135,10 +135,7 @@ modindex_common_prefix = ['mopidy.'] # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -if on_rtd: - html_theme = 'default' -else: - html_theme = 'nature' +html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -157,7 +154,8 @@ html_theme_path = ['_themes'] # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = '_static/mopidy.png' +if on_rtd: + 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 9bcd0fe3a718d28a4d57a02c0e7490f5a19d6840 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 6 Feb 2012 13:22:42 +0100 Subject: [PATCH 30/60] Replace aptitude with apt-get --- docs/development/contributing.rst | 4 ++-- docs/installation/index.rst | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/development/contributing.rst b/docs/development/contributing.rst index 0303bdc7..782d2f20 100644 --- a/docs/development/contributing.rst +++ b/docs/development/contributing.rst @@ -74,7 +74,7 @@ Running tests To run tests, you need a couple of dependencies. They can be installed through Debian/Ubuntu package management:: - sudo aptitude install python-coverage python-mock python-nose + sudo apt-get install python-coverage python-mock python-nose Or, they can be installed using ``pip``:: @@ -126,7 +126,7 @@ from the documentation files, you need some additional dependencies. You can install them through Debian/Ubuntu package management:: - sudo aptitude install python-sphinx python-pygraphviz graphviz + sudo apt-get install python-sphinx python-pygraphviz graphviz Then, to generate docs:: diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 8dd47ada..b9d76e60 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -100,7 +100,7 @@ install Mopidy from PyPI using Pip. #. Then, you need to install Pip:: - sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian + sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian sudo easy_install pip # On OS X #. To install the currently latest stable release of Mopidy:: @@ -134,7 +134,7 @@ Mopidy's ``develop`` branch. #. Then, you need to install Pip:: - sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian + sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian sudo easy_install pip # On OS X #. To install the latest snapshot of Mopidy, run:: @@ -157,7 +157,7 @@ If you want to contribute to Mopidy, you should install Mopidy using Git. #. Then install Git, if haven't already:: - sudo aptitude install git-core # On Ubuntu/Debian + sudo apt-get install git-core # On Ubuntu/Debian sudo brew install git # On OS X using Homebrew #. Clone the official Mopidy repository, or your own fork of it:: From 95ab34dd7095196724a3bd952510e6451ef20684 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 00:11:55 +0100 Subject: [PATCH 31/60] Simplify requirements listing --- docs/installation/index.rst | 42 ++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index b9d76e60..59e51500 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -18,36 +18,36 @@ Requirements gstreamer libspotify -If you install Mopidy from the APT archive, as described below, you can skip -the dependency installation part. +If you install Mopidy from the APT archive, as described below, APT will take +care of all the dependencies for you. Otherwise, make sure you got the required +dependencies installed. -Otherwise, make sure you got the required dependencies installed. +- Hard dependencies: -- Python >= 2.6, < 3 + - Python >= 2.6, < 3 -- `Pykka `_ >= 0.12.3 + - Pykka >= 0.12.3:: -- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`. + sudo pip install -U pykka -- 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: - - - The default backend, :mod:`mopidy.backends.spotify`, requires libspotify - and pyspotify. See :doc:`libspotify`. - - - The local backend, :mod:`mopidy.backends.local`, requires no additional - dependencies. + - GStreamer 0.10.x, with Python bindings. See :doc:`gstreamer`. - Optional dependencies: - - To use the Last.FM scrobbler, see :mod:`mopidy.frontends.lastfm` for - additional requirements. + - For Spotify support, you need libspotify and pyspotify. See + :doc:`libspotify`. - - To use the MPRIS frontend, e.g. using the Ubuntu Sound Menu, see - :mod:`mopidy.frontends.mpris` for additional requirements. + - To scrobble your played tracks to Last.FM, you need pylast:: + + sudo pip install -U pylast + + - To use MPRIS, e.g. for controlling Mopidy from the Ubuntu Sound Menu, you + need some additional requirements:: + + sudo apt-get install python-dbus python-indicate + + - Some custom mixers (but not the default one) require additional + dependencies. See the docs for each mixer. Install latest stable release From 0d16bb0048a973d376a7b7b87628a3360ee6fb13 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 00:27:14 +0100 Subject: [PATCH 32/60] Simplify libspotify/pyspotify installation instructions --- docs/installation/libspotify.rst | 52 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index 2728be94..0b0535d7 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -12,12 +12,6 @@ install libspotify and `pyspotify `_. This backend requires a paid `Spotify premium account `_. -.. note:: - - This product uses SPOTIFY CORE but is not endorsed, certified or otherwise - approved in any way by Spotify. Spotify is the registered trade mark of the - Spotify Group. - Installing libspotify ===================== @@ -26,23 +20,20 @@ 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 libspotify8 - -When libspotify has been installed, continue with -:ref:`pyspotify_installation`. +If you install from APT, jump directly to :ref:`pyspotify_installation` below. On Linux from source -------------------- -Download and install libspotify 0.0.8 for your OS and CPU architecture from -https://developer.spotify.com/en/libspotify/. +First, check pyspotify's changelog to see what's the latest version of +libspotify which is supported. The versions of libspotify and pyspotify are +tightly coupled. -For 64-bit Linux the process is as follows:: +Download and install the appropriate version of libspotify for your OS and CPU +architecture from https://developer.spotify.com/en/libspotify/. + +For libspotify 0.0.8 for 64-bit Linux the process is as follows:: wget http://developer.spotify.com/download/libspotify/libspotify-0.0.8-linux6-x86_64.tar.gz tar zxfv libspotify-0.0.8-linux6-x86_64.tar.gz @@ -50,6 +41,9 @@ For 64-bit Linux the process is as follows:: sudo make install prefix=/usr/local sudo ldconfig +Remember to adjust for the latest libspotify version supported by pyspotify, +your OS and your CPU architecture. + When libspotify has been installed, continue with :ref:`pyspotify_installation`. @@ -84,29 +78,35 @@ by installing pyspotify. On Linux from APT archive ------------------------- -Assuming that you've already set up http://apt.mopidy.com/ as a software -source, run:: +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 python-spotify -If you haven't already installed libspotify, this command will install both -libspotify and pyspotify for you. +This command will install both libspotify and pyspotify for you. -On Linux/OS X from source +On Linux from source ------------------------- +If you have have already installed libspotify, you can continue with installing +the libspotify Python bindings, called pyspotify. + 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. - Then get, build, and install the latest releast of pyspotify using ``pip``:: sudo pip install -U pyspotify -Or using the older ``easy_install``:: - sudo easy_install pyspotify +On OS X from source +------------------- + +If you have already installed libspotify, you can get, build, and install the +latest releast of pyspotify using ``pip``:: + + sudo pip install -U pyspotify From a679d0c2eee75bffccb4d334044f9c0d5f3d8a6c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 00:46:38 +0100 Subject: [PATCH 33/60] Add detailed GStreamer installation instructions for OS X --- docs/installation/gstreamer.rst | 70 ++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index 08e16378..7daac9cf 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -2,7 +2,8 @@ GStreamer installation ********************** -To use the Mopidy, you first need to install GStreamer and its Python bindings. +To use the Mopidy, you first need to install GStreamer and the GStreamer Python +bindings. Installing GStreamer @@ -15,6 +16,10 @@ 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. + +On Debian/Ubuntu +^^^^^^^^^^^^^^^^ + If you use Debian/Ubuntu you can install GStreamer like this:: sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \ @@ -24,30 +29,67 @@ 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. +On Arch Linux +^^^^^^^^^^^^^ + +If you use Arch Linux, install the following packages from the official +repository:: + + sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \ + gstreamer0.10-ugly-plugins + + On OS X from Homebrew --------------------- .. note:: - We have created GStreamer formulas for Homebrew to make the GStreamer - 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 - back to using MacPorts. + We have been working with `Homebrew `_ to + make all the GStreamer packages easily installable on OS X using Homebrew. + We've gotten most of our packages included, but the Homebrew guys aren't + very happy to include Python specific packages into Homebrew, even though + they are not installable by pip. If you're interested, see the discussion + in `Homebrew's issue #1612 + `_ for details. -To install GStreamer on OS X using Homebrew:: +The following is currently the shortest path to installing GStreamer with +Python bindings on OS X using Homebrew. - brew install gst-python gst-plugins-good gst-plugins-ugly +#. Install `Homebrew `_. +#. Download our Homebrew formulas for `pycairo`, `pygobject`, `pygtk`, and + `gst-python`:: -On OS X from MacPorts ---------------------- + wget -O/usr/local/Library/Formula/pycairo.rb \ + https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pycairo.rb + wget -O/usr/local/Library/Formula/pygobject.rb \ + https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pygobject.rb + wget -O/usr/local/Library/Formula/pygtk.rb \ + https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pygtk.rb + wget -O/usr/local/Library/Formula/gst-python.rb \ + https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/gst-python.rb -To install GStreamer on OS X using MacPorts:: +#. Install the required packages:: - sudo port install py26-gst-python gstreamer-plugins-good \ - gstreamer-plugins-ugly + brew install gst-python gst-plugins-good gst-plugins-ugly + +#. Make sure to include Homebrew's Python ``site-packages`` directory in your + ``PYTHONPATH``. If you don't include this, Mopidy will not find GStreamer + and crash. + + You can either amend your ``PYTHONPATH`` permanently, by adding the + following statement to your shell's init file, e.g. ``~/.bashrc``:: + + export PYTHONPATH=$(brew --prefix)/lib/python2.6/site-packages:$PYTHONPATH + + Or, you can prefix the Mopidy command every time you run it:: + + PYTHONPATH=$(brew --prefix)/lib/python2.6/site-packages mopidy + + Note that you need to replace ``python2.6`` with ``python2.7`` if that's + the Python version you are using. To find your Python version, run:: + + python --version Testing the installation From 3f86d3fd321d778f2e8f54eab2ad8f5a20d6d9b4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 00:51:34 +0100 Subject: [PATCH 34/60] A bit less easy_install in the world --- docs/installation/index.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 59e51500..8e62421c 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -101,7 +101,7 @@ install Mopidy from PyPI using Pip. #. Then, you need to install Pip:: sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian - sudo easy_install pip # On OS X + sudo easy_install pip # On OS X #. To install the currently latest stable release of Mopidy:: @@ -112,8 +112,6 @@ install Mopidy from PyPI using Pip. #. 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. - Install development version =========================== @@ -135,7 +133,7 @@ Mopidy's ``develop`` branch. #. Then, you need to install Pip:: sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian - sudo easy_install pip # On OS X + sudo easy_install pip # On OS X #. To install the latest snapshot of Mopidy, run:: From cfea4be681e010884510effad3266eeff2c8e5dd Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 00:55:19 +0100 Subject: [PATCH 35/60] "brew upgrade" has replaced " brew install `brew outdated`" --- 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 0b0535d7..5543f38e 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -60,7 +60,7 @@ libspotify:: To update your existing libspotify installation using Homebrew:: brew update - brew install `brew outdated` + brew upgrade When libspotify has been installed, continue with :ref:`pyspotify_installation`. From 5a15964910994720e63ab38fb7e46bcd69dd994f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 00:56:58 +0100 Subject: [PATCH 36/60] Add missing word --- docs/installation/libspotify.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index 5543f38e..223e4ed7 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -79,8 +79,8 @@ 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:: +http://apt.mopidy.com/ for how to use the Mopidy APT archive as a software +source on your system. Then, simply run:: sudo apt-get install python-spotify From 05b0d20fa5d14dca65dfd9e3c201f5f84c42016b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 01:01:52 +0100 Subject: [PATCH 37/60] Remove use of /usr/local, as Homebrew can be installed in other locations --- docs/installation/gstreamer.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index 7daac9cf..72b2feb8 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -60,13 +60,13 @@ Python bindings on OS X using Homebrew. #. Download our Homebrew formulas for `pycairo`, `pygobject`, `pygtk`, and `gst-python`:: - wget -O/usr/local/Library/Formula/pycairo.rb \ + wget -O $(brew --prefix)/Library/Formula/pycairo.rb \ https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pycairo.rb - wget -O/usr/local/Library/Formula/pygobject.rb \ + wget -O $(brew --prefix)/Library/Formula/pygobject.rb \ https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pygobject.rb - wget -O/usr/local/Library/Formula/pygtk.rb \ + wget -O $(brew --prefix)/Library/Formula/pygtk.rb \ https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pygtk.rb - wget -O/usr/local/Library/Formula/gst-python.rb \ + wget -O $(brew --prefix)/Library/Formula/gst-python.rb \ https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/gst-python.rb #. Install the required packages:: From 48db56cc887e7242948a0ea785a0bd6a21e4104d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 01:10:46 +0100 Subject: [PATCH 38/60] GitHub is on HTTPS now --- docs/installation/gstreamer.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index 72b2feb8..82fbab57 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -50,7 +50,7 @@ On OS X from Homebrew very happy to include Python specific packages into Homebrew, even though they are not installable by pip. If you're interested, see the discussion in `Homebrew's issue #1612 - `_ for details. + `_ for details. The following is currently the shortest path to installing GStreamer with Python bindings on OS X using Homebrew. From 5e6fd8d7bca987c8b4c335590d4d6ffb0ffd282d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 01:13:17 +0100 Subject: [PATCH 39/60] Formatting fix --- docs/installation/gstreamer.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index 82fbab57..37cc9a6f 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -57,8 +57,8 @@ Python bindings on OS X using Homebrew. #. Install `Homebrew `_. -#. Download our Homebrew formulas for `pycairo`, `pygobject`, `pygtk`, and - `gst-python`:: +#. Download our Homebrew formulas for ``pycairo``, ``pygobject``, ``pygtk``, + and ``gst-python``:: wget -O $(brew --prefix)/Library/Formula/pycairo.rb \ https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pycairo.rb From bb2c05b215c0b3ae6470cda247e35fea48e3413c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 01:14:11 +0100 Subject: [PATCH 40/60] Pull a couple of section up a level --- docs/installation/gstreamer.rst | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index 37cc9a6f..d70dd156 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -6,19 +6,16 @@ To use the Mopidy, you first need to install GStreamer and the GStreamer Python bindings. -Installing GStreamer -==================== - -On Linux --------- +Installing GStreamer on Linux +============================= 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. -On Debian/Ubuntu -^^^^^^^^^^^^^^^^ +Debian/Ubuntu +------------- If you use Debian/Ubuntu you can install GStreamer like this:: @@ -29,8 +26,8 @@ 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. -On Arch Linux -^^^^^^^^^^^^^ +Arch Linux +---------- If you use Arch Linux, install the following packages from the official repository:: @@ -39,8 +36,8 @@ repository:: gstreamer0.10-ugly-plugins -On OS X from Homebrew ---------------------- +Installing GStreamer on OS X from Homebrew +------------------------------------------ .. note:: From f84cd6833a101b00b065fb250c0ea650392989da Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 01:23:48 +0100 Subject: [PATCH 41/60] Fix header level --- docs/installation/gstreamer.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index d70dd156..fb8df33d 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -36,8 +36,8 @@ repository:: gstreamer0.10-ugly-plugins -Installing GStreamer on OS X from Homebrew ------------------------------------------- +Installing GStreamer on OS X +============================ .. note:: From ca0d2935f8f67ebce30a3ca2a75fd109c764802b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 21:40:26 +0100 Subject: [PATCH 42/60] Fix URLs and use curl instead of wget, as wget isn't installed by default on OS X --- docs/installation/gstreamer.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index fb8df33d..d0dc0461 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -57,14 +57,14 @@ Python bindings on OS X using Homebrew. #. Download our Homebrew formulas for ``pycairo``, ``pygobject``, ``pygtk``, and ``gst-python``:: - wget -O $(brew --prefix)/Library/Formula/pycairo.rb \ - https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pycairo.rb - wget -O $(brew --prefix)/Library/Formula/pygobject.rb \ - https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pygobject.rb - wget -O $(brew --prefix)/Library/Formula/pygtk.rb \ - https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pygtk.rb - wget -O $(brew --prefix)/Library/Formula/gst-python.rb \ - https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/gst-python.rb + curl -o $(brew --prefix)/Library/Formula/pycairo.rb \ + https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pycairo.rb + curl -o $(brew --prefix)/Library/Formula/pygobject.rb \ + https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pygobject.rb + curl -o $(brew --prefix)/Library/Formula/pygtk.rb \ + https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pygtk.rb + curl -o $(brew --prefix)/Library/Formula/gst-python.rb \ + https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/gst-python.rb #. Install the required packages:: From f1ba8af0dec1089e5199b0d87f7ca474fcb9c801 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 8 Feb 2012 13:24:28 +0100 Subject: [PATCH 43/60] Ignore MPRIS tests on OS X --- tests/frontends/mpris/events_test.py | 10 +++++++++- tests/frontends/mpris/player_interface_test.py | 10 +++++++++- tests/frontends/mpris/root_interface_test.py | 11 +++++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/tests/frontends/mpris/events_test.py b/tests/frontends/mpris/events_test.py index 90cdab6a..49e56226 100644 --- a/tests/frontends/mpris/events_test.py +++ b/tests/frontends/mpris/events_test.py @@ -1,11 +1,19 @@ +import sys + import mock -from mopidy.frontends.mpris import MprisFrontend, objects +from mopidy import OptionalDependencyError from mopidy.models import Track +try: + from mopidy.frontends.mpris import MprisFrontend, objects +except OptionalDependencyError: + pass + from tests import unittest +@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux') class BackendEventsTest(unittest.TestCase): def setUp(self): self.mpris_frontend = MprisFrontend() # As a plain class, not an actor diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index a966403e..24c426fb 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -1,11 +1,18 @@ +import sys + import mock +from mopidy import OptionalDependencyError from mopidy.backends.dummy import DummyBackend from mopidy.backends.base.playback import PlaybackController -from mopidy.frontends.mpris import objects from mopidy.mixers.dummy import DummyMixer from mopidy.models import Album, Artist, Track +try: + from mopidy.frontends.mpris import objects +except OptionalDependencyError: + pass + from tests import unittest PLAYING = PlaybackController.PLAYING @@ -13,6 +20,7 @@ PAUSED = PlaybackController.PAUSED STOPPED = PlaybackController.STOPPED +@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux') class PlayerInterfaceTest(unittest.TestCase): def setUp(self): objects.MprisObject._connect_to_dbus = mock.Mock() diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py index 443efdd3..1e54fc15 100644 --- a/tests/frontends/mpris/root_interface_test.py +++ b/tests/frontends/mpris/root_interface_test.py @@ -1,12 +1,19 @@ +import sys + import mock -from mopidy import settings +from mopidy import OptionalDependencyError, settings from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpris import objects + +try: + from mopidy.frontends.mpris import objects +except OptionalDependencyError: + pass from tests import unittest +@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux') class RootInterfaceTest(unittest.TestCase): def setUp(self): objects.exit_process = mock.Mock() From 8c2a333938293f6e6286412648c22bb8af954218 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Feb 2012 23:12:13 +0100 Subject: [PATCH 44/60] get_or_create_folder should also create intermediate folders --- docs/changes.rst | 4 ++++ mopidy/utils/path.py | 7 +++++-- tests/utils/path_test.py | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 8235e95d..528edaa8 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -22,6 +22,10 @@ v0.7.0 (in development) need for copying the entire current playlist from one thread to another. Thanks to John Bäckstrand for pinpointing the issue. +- Fix crash on creation of config and cache directories if intermediate + directories does not exist. This was especially the case on OS X, where + ``~/.config`` doesn't exist for most users. + v0.6.1 (2011-12-28) =================== diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 8bd39f06..5d99ac12 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -8,9 +8,12 @@ logger = logging.getLogger('mopidy.utils.path') def get_or_create_folder(folder): folder = os.path.expanduser(folder) - if not os.path.isdir(folder): + if os.path.isfile(folder): + raise OSError('A file with the same name as the desired ' \ + 'dir, "%s", already exists.' % folder) + elif not os.path.isdir(folder): logger.info(u'Creating dir %s', folder) - os.mkdir(folder, 0755) + os.makedirs(folder, 0755) return folder def get_or_create_file(filename): diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index ba1fcf97..19bae375 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -28,12 +28,32 @@ class GetOrCreateFolderTest(unittest.TestCase): self.assert_(os.path.isdir(folder)) self.assertEqual(created, folder) + def test_creating_nested_folders(self): + level2_folder = os.path.join(self.parent, 'test') + level3_folder = os.path.join(self.parent, 'test', 'test') + self.assert_(not os.path.exists(level2_folder)) + self.assert_(not os.path.isdir(level2_folder)) + self.assert_(not os.path.exists(level3_folder)) + self.assert_(not os.path.isdir(level3_folder)) + created = get_or_create_folder(level3_folder) + self.assert_(os.path.exists(level2_folder)) + self.assert_(os.path.isdir(level2_folder)) + self.assert_(os.path.exists(level3_folder)) + self.assert_(os.path.isdir(level3_folder)) + self.assertEqual(created, level3_folder) + def test_creating_existing_folder(self): created = get_or_create_folder(self.parent) self.assert_(os.path.exists(self.parent)) self.assert_(os.path.isdir(self.parent)) self.assertEqual(created, self.parent) + def test_create_folder_with_name_of_existing_file_throws_oserror(self): + conflicting_file = os.path.join(self.parent, 'test') + open(conflicting_file, 'w').close() + folder = os.path.join(self.parent, 'test') + self.assertRaises(OSError, get_or_create_folder, folder) + class PathToFileURITest(unittest.TestCase): def test_simple_path(self): From 643f71363fdc9075568461531d4bb738c33d10e9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Feb 2012 00:02:33 +0100 Subject: [PATCH 45/60] Make pylint ignore TODO/XXX/FIXME in the code, so we can see more important errors through the noise --- pylintrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylintrc b/pylintrc index d2f84b77..98e10416 100644 --- a/pylintrc +++ b/pylintrc @@ -18,6 +18,7 @@ # R0921 - Abstract class not referenced # W0141 - Used builtin function '%s' # 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,W0613 +disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0511,W0613 From 413603f9817846c74aca844a0c61e15d29fbd596 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Feb 2012 00:02:54 +0100 Subject: [PATCH 46/60] Remove old redundant comment --- tests/gstreamer_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py index 66e0995e..012c9002 100644 --- a/tests/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -6,8 +6,6 @@ from mopidy.utils.path import path_to_uri from tests import unittest, path_to_data_dir -# TODO BaseOutputTest? - @unittest.skipIf(sys.platform == 'win32', 'Our Windows build server does not support GStreamer yet') From 24d9f8f200a48d93c0e64995cc0eb8d37932d1a5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Feb 2012 00:17:21 +0100 Subject: [PATCH 47/60] Fix a bunch of pylint warnings --- mopidy/backends/base/playback.py | 2 -- mopidy/backends/spotify/__init__.py | 1 - mopidy/backends/spotify/library.py | 1 - mopidy/backends/spotify/session_manager.py | 1 - mopidy/backends/spotify/translator.py | 1 - mopidy/core.py | 6 ++---- mopidy/frontends/mpd/dispatcher.py | 3 ++- mopidy/frontends/mpris/objects.py | 3 ++- mopidy/gstreamer.py | 3 ++- mopidy/utils/settings.py | 1 - 10 files changed, 8 insertions(+), 14 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 51fe0d3b..16ac75d1 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -2,8 +2,6 @@ import logging import random import time -from pykka.registry import ActorRegistry - from mopidy.listeners import BackendListener logger = logging.getLogger('mopidy.backends.base') diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index ad45014e..56775926 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -10,7 +10,6 @@ from mopidy.gstreamer import GStreamer logger = logging.getLogger('mopidy.backends.spotify') -ENCODING = 'utf-8' BITRATES = {96: 2, 160: 0, 320: 1} class SpotifyBackend(ThreadingActor, Backend): diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index 59aa9a2c..a080c7bd 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -4,7 +4,6 @@ import Queue from spotify import Link, SpotifyError from mopidy.backends.base import BaseLibraryProvider -from mopidy.backends.spotify import ENCODING from mopidy.backends.spotify.translator import SpotifyTranslator from mopidy.models import Playlist diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 4b81db1f..af731e1b 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -1,4 +1,3 @@ -import glib import logging import os import threading diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index 27f4719b..2f47a42b 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -4,7 +4,6 @@ import logging from spotify import Link, SpotifyError from mopidy import settings -from mopidy.backends.spotify import ENCODING from mopidy.models import Artist, Album, Track, Playlist logger = logging.getLogger('mopidy.backends.spotify.translator') diff --git a/mopidy/core.py b/mopidy/core.py index 08c5e0d7..596e0fe5 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -12,14 +12,12 @@ gobject.threads_init() # so that GStreamer doesn't hijack e.g. ``--help``. # NOTE This naive fix does not support values like ``bar`` in # ``--gst-foo bar``. Use equals to pass values, like ``--gst-foo=bar``. -def is_gst_arg(arg): - return arg.startswith('--gst') or arg == '--help-gst' +def is_gst_arg(argument): + return argument.startswith('--gst') or argument == '--help-gst' gstreamer_args = [arg for arg in sys.argv[1:] if is_gst_arg(arg)] mopidy_args = [arg for arg in sys.argv[1:] if not is_gst_arg(arg)] sys.argv[1:] = gstreamer_args -from pykka.registry import ActorRegistry - from mopidy import (get_version, settings, OptionalDependencyError, SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE) from mopidy.gstreamer import GStreamer diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 5ee70a5b..2b012c7c 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -244,7 +244,8 @@ class MpdContext(object): """ if self._backend is None: backend_refs = ActorRegistry.get_by_class(Backend) - assert len(backend_refs) == 1, 'Expected exactly one running backend.' + assert len(backend_refs) == 1, \ + 'Expected exactly one running backend.' self._backend = backend_refs[0].proxy() return self._backend diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index e0a83e03..9ed1fe2c 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -81,7 +81,8 @@ class MprisObject(dbus.service.Object): def _connect_to_dbus(self): logger.debug(u'Connecting to D-Bus...') mainloop = dbus.mainloop.glib.DBusGMainLoop() - bus_name = dbus.service.BusName(BUS_NAME, dbus.SessionBus(mainloop=mainloop)) + bus_name = dbus.service.BusName(BUS_NAME, + dbus.SessionBus(mainloop=mainloop)) logger.info(u'Connected to D-Bus') return bus_name diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index ffb8c4f1..b6a8ab2b 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -332,7 +332,8 @@ class GStreamer(ThreadingActor): self._tee.send_event(event) def _handle_event_probe(self, teesrc, event): - if event.type == gst.EVENT_CUSTOM_DOWNSTREAM and event.has_name('mopidy-unlink-tee'): + if (event.type == gst.EVENT_CUSTOM_DOWNSTREAM + and event.has_name('mopidy-unlink-tee')): data = self._get_structure_data(event.get_structure()) output = teesrc.get_peer().get_parent() diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index fca4f337..ff449a61 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from copy import copy import getpass -import glib import logging import os from pprint import pformat From 2eae7aaae139be889b60f2806a48b837f0be5529 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Feb 2012 00:30:18 +0100 Subject: [PATCH 48/60] Call ThreadingActor/etc constructors to please pylint --- mopidy/frontends/lastfm.py | 1 + mopidy/frontends/mpd/__init__.py | 1 + mopidy/frontends/mpris/__init__.py | 1 + mopidy/gstreamer.py | 1 + mopidy/mixers/alsa.py | 1 + mopidy/mixers/denon.py | 1 + mopidy/mixers/dummy.py | 1 + mopidy/mixers/gstreamer_software.py | 1 + mopidy/mixers/nad.py | 2 ++ mopidy/utils/network.py | 1 + 10 files changed, 11 insertions(+) diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index 125457cd..0e79024b 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -37,6 +37,7 @@ class LastfmFrontend(ThreadingActor, BackendListener): """ def __init__(self): + super(LastfmFrontend, self).__init__() self.lastfm = None self.last_start_time = None diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index b6adc09d..99134012 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -25,6 +25,7 @@ class MpdFrontend(actor.ThreadingActor, listeners.BackendListener): """ def __init__(self): + super(MpdFrontend, self).__init__() hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME) port = settings.MPD_SERVER_PORT diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py index 579038ca..0f5d35c5 100644 --- a/mopidy/frontends/mpris/__init__.py +++ b/mopidy/frontends/mpris/__init__.py @@ -57,6 +57,7 @@ class MprisFrontend(ThreadingActor, BackendListener): """ def __init__(self): + super(MprisFrontend, self).__init__() self.indicate_server = None self.mpris_object = None diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index b6a8ab2b..282acd4e 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -25,6 +25,7 @@ class GStreamer(ThreadingActor): """ def __init__(self): + super(GStreamer, self).__init__() self._default_caps = gst.Caps(""" audio/x-raw-int, endianness=(int)1234, diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py index ae4bd031..acb12e66 100644 --- a/mopidy/mixers/alsa.py +++ b/mopidy/mixers/alsa.py @@ -23,6 +23,7 @@ class AlsaMixer(ThreadingActor, BaseMixer): """ def __init__(self): + super(AlsaMixer, self).__init__() self._mixer = None def on_start(self): diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index d0dc5f54..e869dae8 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -26,6 +26,7 @@ class DenonMixer(ThreadingActor, BaseMixer): """ def __init__(self, *args, **kwargs): + super(DenonMixer, self).__init__(*arg, **kwargs) self._device = kwargs.get('device', None) self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] self._volume = 0 diff --git a/mopidy/mixers/dummy.py b/mopidy/mixers/dummy.py index 23f96c4c..7262e83c 100644 --- a/mopidy/mixers/dummy.py +++ b/mopidy/mixers/dummy.py @@ -6,6 +6,7 @@ class DummyMixer(ThreadingActor, BaseMixer): """Mixer which just stores and reports the chosen volume.""" def __init__(self): + super(DummyMixer, self).__init__() self._volume = None def get_volume(self): diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py index 523c3387..a38692db 100644 --- a/mopidy/mixers/gstreamer_software.py +++ b/mopidy/mixers/gstreamer_software.py @@ -8,6 +8,7 @@ class GStreamerSoftwareMixer(ThreadingActor, BaseMixer): """Mixer which uses GStreamer to control volume in software.""" def __init__(self): + super(GStreamerSoftwareMixer, self).__init__() self.output = None def on_start(self): diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index 4dbf27be..78473308 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -37,6 +37,7 @@ class NadMixer(ThreadingActor, BaseMixer): """ def __init__(self): + super(NadMixer, self).__init__() self._volume_cache = None self._nad_talker = NadTalker.start().proxy() @@ -71,6 +72,7 @@ class NadTalker(ThreadingActor): _nad_volume = None def __init__(self): + super(NadTalker, self).__init__() self._device = None def on_start(self): diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 5079fe7c..0a0928ce 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -297,6 +297,7 @@ class LineProtocol(ThreadingActor): encoding = 'utf-8' def __init__(self, connection): + super(LineProtocol, self).__init__() self.connection = connection self.prevent_timeout = False self.recv_buffer = '' From 9e8e02295dd468b72295ad71258258b7544fd5c7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Feb 2012 00:31:42 +0100 Subject: [PATCH 49/60] Fix typo --- mopidy/mixers/denon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index e869dae8..02b86c38 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -26,7 +26,7 @@ class DenonMixer(ThreadingActor, BaseMixer): """ def __init__(self, *args, **kwargs): - super(DenonMixer, self).__init__(*arg, **kwargs) + super(DenonMixer, self).__init__(*args, **kwargs) self._device = kwargs.get('device', None) self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] self._volume = 0 From fc5f6df740c4f4444a96d6b06a157a2d862c3984 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Feb 2012 00:34:33 +0100 Subject: [PATCH 50/60] Simplify DenonMixer constructor args --- mopidy/mixers/denon.py | 6 +++--- tests/mixers/denon_test.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index 02b86c38..b0abbdb9 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -25,9 +25,9 @@ class DenonMixer(ThreadingActor, BaseMixer): - :attr:`mopidy.settings.MIXER_EXT_PORT` -- Example: ``/dev/ttyUSB0`` """ - def __init__(self, *args, **kwargs): - super(DenonMixer, self).__init__(*args, **kwargs) - self._device = kwargs.get('device', None) + def __init__(self, device=None): + super(DenonMixer, self).__init__() + self._device = device self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] self._volume = 0 diff --git a/tests/mixers/denon_test.py b/tests/mixers/denon_test.py index 7fec3c82..cdfe0772 100644 --- a/tests/mixers/denon_test.py +++ b/tests/mixers/denon_test.py @@ -34,7 +34,7 @@ class DenonMixerTest(BaseMixerTest, unittest.TestCase): def setUp(self): self.device = DenonMixerDeviceMock() - self.mixer = DenonMixer(None, device=self.device) + self.mixer = DenonMixer(device=self.device) def test_reopen_device(self): self.device._open = False From 844b219565ed709d8535af552c14c9c281014379 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Feb 2012 00:42:39 +0100 Subject: [PATCH 51/60] Potential fix for gst LinkError (#144) suggested by adamcik --- mopidy/gstreamer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 282acd4e..5d393b66 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -83,6 +83,8 @@ class GStreamer(ThreadingActor): def _on_new_pad(self, source, pad, target_pad): if not pad.is_linked(): + if target_pad.is_linked(): + target_pad.unlink(target_pad.get_peer()) pad.link(target_pad) def _on_message(self, bus, message): From eb8ecc33a2320a9f6aa489180c16defaa6b8b722 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 13 Feb 2012 09:37:21 +0100 Subject: [PATCH 52/60] Switch arguments so that unlink() is passed the sinkpad --- mopidy/gstreamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 5d393b66..c33dbe03 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -84,7 +84,7 @@ class GStreamer(ThreadingActor): def _on_new_pad(self, source, pad, target_pad): if not pad.is_linked(): if target_pad.is_linked(): - target_pad.unlink(target_pad.get_peer()) + target_pad.get_peer().unlink(target_pad) pad.link(target_pad) def _on_message(self, bus, message): From 904adc938ee4930f2b410c4c5d52689a56d4f079 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 13 Feb 2012 09:52:09 +0100 Subject: [PATCH 53/60] Update changelog --- docs/changes.rst | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 528edaa8..b389b7be 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -26,6 +26,11 @@ v0.7.0 (in development) directories does not exist. This was especially the case on OS X, where ``~/.config`` doesn't exist for most users. +**Bug fixes** + +- Fix ``gst.LinkError`` which appeared when using newer versions of GStreamer, + e.g. on Ubuntu 12.04 Alpha. (Fixes: :issue:`144`) + v0.6.1 (2011-12-28) =================== @@ -220,7 +225,7 @@ This is a bug fix release fixing audio problems on older GStreamer and some minor bugs. -**Bugfixes** +**Bug fixes** - Fix broken audio on at least GStreamer 0.10.30, which affects Ubuntu 10.10. The GStreamer `appsrc` bin wasn't being linked due to lack of default caps. @@ -347,7 +352,7 @@ v0.3.1 (2011-01-22) A couple of fixes to the 0.3.0 release is needed to get a smooth installation. -**Bugfixes** +**Bug fixes** - The Spotify application key was missing from the Python package. @@ -516,7 +521,7 @@ v0.2.1 (2011-01-07) This is a maintenance release without any new features. -**Bugfixes** +**Bug fixes** - Fix crash in :mod:`mopidy.frontends.lastfm` which occurred at playback if either :mod:`pylast` was not installed or the Last.fm scrobbling was not @@ -846,7 +851,7 @@ As always, report problems at our IRC channel or our issue tracker. Thanks! - Merged the ``gstreamer`` branch from Thomas Adamcik: - - More than 200 new tests, and thus several bugfixes to existing code. + - More than 200 new tests, and thus several bug fixes to existing code. - Several new generic features, like shuffle, consume, and playlist repeat. (Fixes: :issue:`3`) - **[Work in Progress]** A new backend for playing music from a local music From 219e723974e98d9c6da4e1f94cb6ff4be84c5dab Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 13 Feb 2012 10:30:46 +0100 Subject: [PATCH 54/60] Fix crash on mismatching quotation (fixes #137) --- docs/changes.rst | 3 +++ mopidy/frontends/mpd/protocol/music_db.py | 10 ++++++++-- tests/frontends/mpd/protocol/regression_test.py | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index b389b7be..69de8558 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -31,6 +31,9 @@ v0.7.0 (in development) - Fix ``gst.LinkError`` which appeared when using newer versions of GStreamer, e.g. on Ubuntu 12.04 Alpha. (Fixes: :issue:`144`) +- Fix crash on mismatching quotation in ``list`` MPD queries. (Fixes: + :issue:`137`) + v0.6.1 (2011-12-28) =================== diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 299fce97..cde2754a 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -189,8 +189,14 @@ def _list_build_query(field, mpd_query): """Converts a ``list`` query to a Mopidy query.""" if mpd_query is None: return {} - # shlex does not seem to be friends with unicode objects - tokens = shlex.split(mpd_query.encode('utf-8')) + try: + # shlex does not seem to be friends with unicode objects + tokens = shlex.split(mpd_query.encode('utf-8')) + except ValueError as error: + if error.message == 'No closing quotation': + raise MpdArgError(u'Invalid unquoted character', command=u'list') + else: + raise error tokens = [t.decode('utf-8') for t in tokens] if len(tokens) == 1: if field == u'album': diff --git a/tests/frontends/mpd/protocol/regression_test.py b/tests/frontends/mpd/protocol/regression_test.py index d4e4b2aa..7f214efa 100644 --- a/tests/frontends/mpd/protocol/regression_test.py +++ b/tests/frontends/mpd/protocol/regression_test.py @@ -146,3 +146,19 @@ class IssueGH113RegressionTest(protocol.BaseTestCase): self.sendRequest( r'listplaylistinfo "all lart spotify:track:\\w\\{22\\} pastes"') self.assertInResponse('OK') + + +class IssueGH137RegressionTest(protocol.BaseTestCase): + """ + The issue: https://github.com/mopidy/mopidy/issues/137 + + How to reproduce: + + - Send "list" query with mismatching quotes + """ + + def test(self): + self.sendRequest(u'list Date Artist "Anita Ward" ' + u'Album "This Is Remixed Hits - Mashups & Rare 12" Mixes"') + + self.assertInResponse('ACK [2@0] {list} Invalid unquoted character') From 86a4d6c36edf2619e45009d995ecf82f7dae908a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 13 Feb 2012 10:33:17 +0100 Subject: [PATCH 55/60] Avoid HTTP to HTTPS redirect --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index b4dbc35b..f8b4ffc3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -241,4 +241,4 @@ latex_documents = [ needs_sphinx = '1.0' -extlinks = {'issue': ('http://github.com/mopidy/mopidy/issues/%s', 'GH-')} +extlinks = {'issue': ('https://github.com/mopidy/mopidy/issues/%s', 'GH-')} From 029192876c2d0d2438b15928dc0685f61f490241 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 13 Feb 2012 10:34:17 +0100 Subject: [PATCH 56/60] Remove 'Bug fixes' header only used for bugfix releases --- docs/changes.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 69de8558..20ad3169 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -26,8 +26,6 @@ v0.7.0 (in development) directories does not exist. This was especially the case on OS X, where ``~/.config`` doesn't exist for most users. -**Bug fixes** - - Fix ``gst.LinkError`` which appeared when using newer versions of GStreamer, e.g. on Ubuntu 12.04 Alpha. (Fixes: :issue:`144`) From 0269686453a5035518106a5e1aa5243684b3d20c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 24 Feb 2012 23:50:45 +0100 Subject: [PATCH 57/60] Fix volume response when capping volume --- docs/changes.rst | 6 ++++++ mopidy/mixers/base.py | 19 ++++++++++++++++--- tests/mixers/dummy_test.py | 7 ++++++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 20ad3169..d955978d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -32,6 +32,12 @@ v0.7.0 (in development) - Fix crash on mismatching quotation in ``list`` MPD queries. (Fixes: :issue:`137`) +- Volume is now reported to be the same as the volume was set to, also when + internal rounding have been done due to + :attr:`mopidy.settings.MIXER_MAX_VOLUME` has been set to cap the volume. This + should make it possible to manage capped volume from clients that only + increase volume with one step at a time, like ncmpcpp does. + v0.6.1 (2011-12-28) =================== diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py index 8798076a..48df5325 100644 --- a/mopidy/mixers/base.py +++ b/mopidy/mixers/base.py @@ -21,19 +21,32 @@ class BaseMixer(object): Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is equal to 0. Values above 100 is equal to 100. """ + if not hasattr(self, '_user_volume'): + self._user_volume = 0 volume = self.get_volume() if volume is None: return None - return int(volume / self.amplification_factor) + elif not self.amplification_factor < 1: + return volume + else: + user_volume = int(volume / self.amplification_factor) + if (user_volume - 1) <= self._user_volume <= (user_volume + 1): + return self._user_volume + else: + return user_volume @volume.setter def volume(self, volume): - volume = int(int(volume) * self.amplification_factor) + if not hasattr(self, '_user_volume'): + self._user_volume = 0 + volume = int(volume) if volume < 0: volume = 0 elif volume > 100: volume = 100 - self.set_volume(volume) + self._user_volume = volume + real_volume = int(volume * self.amplification_factor) + self.set_volume(real_volume) self._trigger_volume_changed() def get_volume(self): diff --git a/tests/mixers/dummy_test.py b/tests/mixers/dummy_test.py index 8ae8623c..f9418d7a 100644 --- a/tests/mixers/dummy_test.py +++ b/tests/mixers/dummy_test.py @@ -4,7 +4,7 @@ from tests import unittest from tests.mixers.base_test import BaseMixerTest -class DenonMixerTest(BaseMixerTest, unittest.TestCase): +class DummyMixerTest(BaseMixerTest, unittest.TestCase): mixer_class = DummyMixer def test_set_volume_is_capped(self): @@ -16,3 +16,8 @@ class DenonMixerTest(BaseMixerTest, unittest.TestCase): self.mixer.amplification_factor = 0.5 self.mixer._volume = 50 self.assertEquals(self.mixer.volume, 100) + + def test_get_volume_get_the_same_number_as_was_set(self): + self.mixer.amplification_factor = 0.5 + self.mixer.volume = 13 + self.assertEquals(self.mixer.volume, 13) From d0b8d1942f702c544d2de0248314eb54db7abdee Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 25 Feb 2012 00:24:41 +0100 Subject: [PATCH 58/60] Fix error in logger name --- mopidy/mixers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py index 48df5325..4690be61 100644 --- a/mopidy/mixers/base.py +++ b/mopidy/mixers/base.py @@ -2,7 +2,7 @@ import logging from mopidy import listeners, settings -logger = logging.getLogger('mopdy.mixers') +logger = logging.getLogger('mopidy.mixers') class BaseMixer(object): """ From b82b4d87cecc8eec6411e83e9c3b8b96981ab005 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 25 Feb 2012 00:25:12 +0100 Subject: [PATCH 59/60] Simplify if statement --- mopidy/mixers/base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py index 4690be61..82783be1 100644 --- a/mopidy/mixers/base.py +++ b/mopidy/mixers/base.py @@ -24,9 +24,7 @@ class BaseMixer(object): if not hasattr(self, '_user_volume'): self._user_volume = 0 volume = self.get_volume() - if volume is None: - return None - elif not self.amplification_factor < 1: + if volume is None or not self.amplification_factor < 1: return volume else: user_volume = int(volume / self.amplification_factor) From 0f6e6ab4256ae36a5c3d51367a48e1018a325495 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 25 Feb 2012 01:02:41 +0100 Subject: [PATCH 60/60] Update changelog for v0.7.0 release --- docs/changes.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index d955978d..65bd6b4b 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,12 +4,13 @@ Changes This change log is used to track all major changes to Mopidy. -v0.7.0 (in development) -======================= +v0.7.0 (2012-02-25) +=================== -**Important changes** - -- Nothing yet. +Not a big release with regard to features, but this release got some +performance improvements over v0.6, especially for slower Atom systems. It also +fixes a couple of other bugs, including one which made Mopidy crash when using +GStreamer from the prereleases of Ubuntu 12.04. **Changes**