diff --git a/MANIFEST.in b/MANIFEST.in index 1c126f85..033c51f2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include LICENSE pylintrc *.rst data/mopidy.desktop +include LICENSE pylintrc *.rst *.ini data/mopidy.desktop include mopidy/backends/spotify/spotify_appkey.key recursive-include docs * prune docs/_build diff --git a/docs/changes.rst b/docs/changes.rst index fe7b9927..12da4e6d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,11 +5,35 @@ Changes This change log is used to track all major changes to Mopidy. -0.4.0 (in development) +0.5.0 (in development) ====================== No description yet. +**Changes** + +No changes yet. + + +0.4.0 (2011-04-27) +================== + +Mopidy 0.4.0 is another release without major feature additions. In 0.4.0 we've +fixed a bunch of issues and bugs, with the help of several new contributors +who are credited in the changelog below. The major change of 0.4.0 is an +internal refactoring which clears way for future features, and which also make +Mopidy work on Python 2.7. In other words, Mopidy 0.4.0 works on Ubuntu 11.04 +and Arch Linux. + +Please note that 0.4.0 requires some updated dependencies, as listed under +*Important changes* below. Also, the known bug in the Spotify playlist +loading from Mopidy 0.3.0 is still present. + +.. warning:: Known bug in Spotify playlist loading + + There is a known bug in the loading of Spotify playlists. To avoid the bug, + follow the simple workaround described at :issue:`59`. + **Important changes** @@ -30,7 +54,7 @@ No description yet. - Mopidy now use Pykka actors for thread management and inter-thread communication. The immediate advantage of this is that Mopidy now works on - Python 2.7. (Fixes: :issue:`66`) + Python 2.7, which is the default on e.g. Ubuntu 11.04. (Fixes: :issue:`66`) - Spotify backend: @@ -43,6 +67,8 @@ No description yet. - Reduce log level for trivial log messages from warning to info. (Fixes: :issue:`71`) + - Pause playback on network connection errors. (Fixes: :issue:`65`) + - Local backend: - Fix crash in :command:`mopidy-scan` if a track has no artist name. Thanks @@ -64,6 +90,14 @@ No description yet. - Fix bug where ``status`` returned ``song: None``, which caused MPDroid to crash. (Fixes: :issue:`69`) + - Gracefully fallback to IPv4 sockets on systems that supports IPv6, but has + turned it off. (Fixes: :issue:`75`) + +- GStreamer output: + + - Use ``uridecodebin`` for playing audio from both Spotify and the local + backend. This contributes to support for multiple backends simultaneously. + - Settings: - Fix crash on ``--list-settings`` on clean installation. Thanks to Martins @@ -74,6 +108,11 @@ No description yet. - Replace test data symlinks with real files to avoid symlink issues when installing with pip. (Fixes: :issue:`68`) +- Debugging: + + - Include platform, architecture, Linux distribution, and Python version in + the debug log, to ease debugging of issues with attached debug logs. + 0.3.1 (2010-01-22) ================== diff --git a/mopidy/__init__.py b/mopidy/__init__.py index e9ced3ae..79a0aa29 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -1,10 +1,17 @@ +import platform import sys if not (2, 6) <= sys.version_info < (3,): sys.exit(u'Mopidy requires Python >= 2.6, < 3') from subprocess import PIPE, Popen -VERSION = (0, 4, 0) +VERSION = (0, 5, 0) + +def get_version(): + try: + return get_git_version() + except EnvironmentError: + return get_plain_version() def get_git_version(): process = Popen(['git', 'describe'], stdout=PIPE, stderr=PIPE) @@ -18,11 +25,13 @@ def get_git_version(): def get_plain_version(): return '.'.join(map(str, VERSION)) -def get_version(): - try: - return get_git_version() - except EnvironmentError: - return get_plain_version() +def get_platform(): + return platform.platform() + +def get_python(): + implementation = platform.python_implementation() + version = platform.python_version() + return u' '.join([implementation, version]) class MopidyException(Exception): def __init__(self, message, *args, **kwargs): diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index 69050eb8..3721fe9c 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -20,7 +20,7 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): self.backend.spotify.session.load( Link.from_string(track.uri).as_track()) self.backend.spotify.session.play(1) - self.backend.output.set_state('PLAYING') + self.backend.output.play_uri('appsrc://') return True except SpotifyError as e: logger.info('Playback of %s failed: %s', track.uri, e) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index e92fe89e..395f3f28 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -74,7 +74,11 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): def connection_error(self, session, error): """Callback used by pyspotify""" - logger.error(u'Spotify connection error: %s', error) + if error is None: + logger.info(u'Spotify connection error resolved') + else: + logger.error(u'Spotify connection error: %s', error) + self.backend.playback.pause() def message_to_user(self, session, message): """Callback used by pyspotify""" diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index 651154f8..dca2b285 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -57,7 +57,9 @@ class SpotifyTranslator(object): return Playlist( uri=str(Link.from_playlist(spotify_playlist)), name=spotify_playlist.name().decode(ENCODING), - tracks=[cls.to_mopidy_track(t) for t in spotify_playlist], + # FIXME if check on link is a hackish workaround for is_local + tracks=[cls.to_mopidy_track(t) for t in spotify_playlist + if str(Link.from_track(t, 0))], ) except SpotifyError, e: logger.info(u'Failed translating Spotify playlist ' diff --git a/mopidy/core.py b/mopidy/core.py index a1c6b361..f1a9dc36 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -23,13 +23,15 @@ def main(): setup_backend() setup_frontends() try: - time.sleep(10000*24*60*60) + while ActorRegistry.get_all(): + time.sleep(1) + logger.info(u'No actors left. Exiting...') except KeyboardInterrupt: - logger.info(u'Exiting...') + logger.info(u'User interrupt. Exiting...') ActorRegistry.stop_all() def parse_options(): - parser = optparse.OptionParser(version='Mopidy %s' % get_version()) + parser = optparse.OptionParser(version=u'Mopidy %s' % get_version()) parser.add_option('-q', '--quiet', action='store_const', const=0, dest='verbosity_level', help='less output (warning level)') diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 231bdf40..8507e266 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -9,6 +9,20 @@ from .session import MpdSession logger = logging.getLogger('mopidy.frontends.mpd.server') +def _try_ipv6_socket(): + """Determine if system really supports IPv6""" + if not socket.has_ipv6: + return False + try: + socket.socket(socket.AF_INET6).close() + return True + except IOError, e: + logger.debug(u'Platform supports IPv6, but socket ' + 'creation failed, disabling: %s', e) + return False + +has_ipv6 = _try_ipv6_socket() + class MpdServer(asyncore.dispatcher): """ The MPD server. Creates a :class:`mopidy.frontends.mpd.session.MpdSession` @@ -21,7 +35,7 @@ class MpdServer(asyncore.dispatcher): def start(self): """Start MPD server.""" try: - if socket.has_ipv6: + if has_ipv6: self.create_socket(socket.AF_INET6, socket.SOCK_STREAM) # Explicitly configure socket to work for both IPv4 and IPv6 self.socket.setsockopt( @@ -53,7 +67,7 @@ class MpdServer(asyncore.dispatcher): self.close() def _format_hostname(self, hostname): - if (socket.has_ipv6 + if (has_ipv6 and re.match('\d+.\d+.\d+.\d+', hostname) is not None): hostname = '::ffff:%s' % hostname return hostname diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 0596addb..a6d1e9dd 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -46,23 +46,9 @@ class GStreamerOutput(ThreadingActor, BaseOutput): pad = self.gst_pipeline.get_by_name('convert').get_pad('sink') - if settings.BACKENDS[0] == 'mopidy.backends.local.LocalBackend': - uri_bin = gst.element_factory_make('uridecodebin', 'uri') - uri_bin.connect('pad-added', self._process_new_pad, pad) - self.gst_pipeline.add(uri_bin) - else: - app_src = gst.element_factory_make('appsrc', 'appsrc') - app_src_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""") - app_src.set_property('caps', app_src_caps) - self.gst_pipeline.add(app_src) - app_src.get_pad('src').link(pad) + uridecodebin = gst.element_factory_make('uridecodebin', 'uri') + uridecodebin.connect('pad-added', self._process_new_pad, pad) + self.gst_pipeline.add(uridecodebin) # Setup bus and message processor gst_bus = self.gst_pipeline.get_bus() @@ -98,12 +84,12 @@ class GStreamerOutput(ThreadingActor, BaseOutput): def deliver_data(self, caps_string, data): """Deliver audio data to be played""" - app_src = self.gst_pipeline.get_by_name('appsrc') + source = self.gst_pipeline.get_by_name('source') caps = gst.caps_from_string(caps_string) buffer_ = gst.Buffer(buffer(data)) buffer_.set_caps(caps) - app_src.set_property('caps', caps) - app_src.emit('push-buffer', buffer_) + source.set_property('caps', caps) + source.emit('push-buffer', buffer_) def end_of_data_stream(self): """ @@ -112,7 +98,7 @@ class GStreamerOutput(ThreadingActor, BaseOutput): We will get a GStreamer message when the stream playback reaches the token, and can then do any end-of-stream related tasks. """ - self.gst_pipeline.get_by_name('appsrc').emit('end-of-stream') + self.gst_pipeline.get_by_name('source').emit('end-of-stream') def get_position(self): try: diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index c74ff5ea..531b68b6 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -1,7 +1,8 @@ import logging import logging.handlers +import platform -from mopidy import get_version, settings +from mopidy import get_version, get_platform, get_python, settings def setup_logging(verbosity_level, save_debug_log): setup_root_logger() @@ -9,7 +10,8 @@ def setup_logging(verbosity_level, save_debug_log): if save_debug_log: setup_debug_logging_to_file() logger = logging.getLogger('mopidy.utils.log') - logger.info(u'-- Starting Mopidy %s --', get_version()) + logger.info(u'Starting Mopidy %s on %s %s', + get_version(), get_platform(), get_python()) def setup_root_logger(): root = logging.getLogger('') diff --git a/tests/frontends/mpd/server_test.py b/tests/frontends/mpd/server_test.py index ef963347..32e90450 100644 --- a/tests/frontends/mpd/server_test.py +++ b/tests/frontends/mpd/server_test.py @@ -10,20 +10,22 @@ class MpdServerTest(unittest.TestCase): self.backend = DummyBackend.start().proxy() self.mixer = DummyMixer.start().proxy() self.server = server.MpdServer() + self.has_ipv6 = server.has_ipv6 def tearDown(self): self.backend.stop().get() self.mixer.stop().get() + server.has_ipv6 = self.has_ipv6 def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self): - server.socket.has_ipv6 = True + server.has_ipv6 = True self.assertEqual(self.server._format_hostname('0.0.0.0'), '::ffff:0.0.0.0') self.assertEqual(self.server._format_hostname('127.0.0.1'), '::ffff:127.0.0.1') def test_format_hostname_does_nothing_when_only_ipv4_available(self): - server.socket.has_ipv6 = False + server.has_ipv6 = False self.assertEquals(self.server._format_hostname('0.0.0.0'), '0.0.0.0') class MpdSessionTest(unittest.TestCase): diff --git a/tests/version_test.py b/tests/version_test.py index f1f86b59..b060a9c6 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -1,7 +1,8 @@ from distutils.version import StrictVersion as SV import unittest +import platform -from mopidy import get_plain_version +from mopidy import get_version, get_plain_version, get_platform, get_python class VersionTest(unittest.TestCase): def test_current_version_is_parsable_as_a_strict_version_number(self): @@ -16,5 +17,15 @@ class VersionTest(unittest.TestCase): self.assert_(SV('0.1.0') < SV('1.0.0')) self.assert_(SV('0.2.0') < SV('0.3.0')) self.assert_(SV('0.3.0') < SV('0.3.1')) - self.assert_(SV('0.3.1') < SV(get_plain_version())) - self.assert_(SV(get_plain_version()) < SV('0.4.1')) + self.assert_(SV('0.3.1') < SV('0.4.0')) + self.assert_(SV('0.4.0') < SV(get_plain_version())) + self.assert_(SV(get_plain_version()) < SV('0.5.1')) + + def test_get_platform_contains_platform(self): + self.assert_(platform.platform() in get_platform()) + + def test_get_python_contains_python_implementation(self): + self.assert_(platform.python_implementation() in get_python()) + + def test_get_python_contains_python_version(self): + self.assert_(platform.python_version() in get_python())