From f73ba3bd62eb2bb13db36d4c3e0c30196becd29a Mon Sep 17 00:00:00 2001 From: Antoine Pierlot-Garcin Date: Mon, 30 May 2011 22:22:23 -0400 Subject: [PATCH 1/8] backend-spotify: implement a container manager (fixes GH59) --- mopidy/backends/spotify/container_manager.py | 16 ++++++++++++++++ mopidy/backends/spotify/session_manager.py | 5 +++++ 2 files changed, 21 insertions(+) create mode 100644 mopidy/backends/spotify/container_manager.py diff --git a/mopidy/backends/spotify/container_manager.py b/mopidy/backends/spotify/container_manager.py new file mode 100644 index 00000000..29360d79 --- /dev/null +++ b/mopidy/backends/spotify/container_manager.py @@ -0,0 +1,16 @@ +import logging + +from spotify.manager import SpotifyContainerManager as PyspotifyContainerManager + +logger = logging.getLogger('mopidy.backends.spotify.container_manager') + +class SpotifyContainerManager(PyspotifyContainerManager): + + def __init__(self, session_manager): + PyspotifyContainerManager.__init__(self) + self.session_manager = session_manager + + def container_loaded(self, container, userdata): + """Callback used by pyspotify.""" + logger.debug(u'Container loaded') + self.session_manager.refresh_stored_playlists() diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index f34283c6..388b29c3 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -12,6 +12,7 @@ from mopidy.backends.spotify.translator import SpotifyTranslator from mopidy.models import Playlist from mopidy.gstreamer import GStreamer from mopidy.utils.process import BaseThread +from mopidy.backends.spotify.container_manager import SpotifyContainerManager logger = logging.getLogger('mopidy.backends.spotify.session_manager') @@ -35,6 +36,8 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): self.connected = threading.Event() self.session = None + self.container_manager = None + def run_inside_try(self): self.setup() self.connect() @@ -61,6 +64,8 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): else: logger.debug(u'Preferring normal bitrate from Spotify') self.session.set_preferred_bitrate(0) + self.container_manager = SpotifyContainerManager(self) + self.container_manager.watch(self.session.playlist_container()) self.connected.set() def logged_out(self, session): From d664c11e2285560e56aaaec646a578fa64b84d7e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 7 Jun 2011 14:09:15 +0200 Subject: [PATCH 2/8] Pull network related functions out of mopidy.frontends.mpd.server --- mopidy/frontends/mpd/server.py | 33 +++------------------------ mopidy/utils/network.py | 36 ++++++++++++++++++++++++++++++ tests/frontends/mpd/server_test.py | 23 ------------------- tests/utils/network_test.py | 19 ++++++++++++++++ 4 files changed, 58 insertions(+), 53 deletions(-) create mode 100644 mopidy/utils/network.py create mode 100644 tests/utils/network_test.py diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 1be46ef4..87a1cd0a 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -1,28 +1,13 @@ import asyncore import logging -import re -import socket import sys from mopidy import settings +from mopidy.utils import network 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` @@ -35,15 +20,9 @@ class MpdServer(asyncore.dispatcher): def start(self): """Start MPD server.""" try: - 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( - socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) - else: - self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket = network.create_socket() self.set_reuse_addr() - hostname = self._format_hostname(settings.MPD_SERVER_HOSTNAME) + hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME) port = settings.MPD_SERVER_PORT logger.debug(u'MPD server is binding to [%s]:%s', hostname, port) self.bind((hostname, port)) @@ -65,9 +44,3 @@ class MpdServer(asyncore.dispatcher): def handle_close(self): """Handle end of client connection.""" self.close() - - def _format_hostname(self, hostname): - if (has_ipv6 - and re.match('\d+.\d+.\d+.\d+', hostname) is not None): - hostname = '::ffff:%s' % hostname - return hostname diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py new file mode 100644 index 00000000..1dedf7d7 --- /dev/null +++ b/mopidy/utils/network.py @@ -0,0 +1,36 @@ +import logging +import re +import socket + +logger = logging.getLogger('mopidy.utils.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 + +#: Boolean value that indicates if creating an IPv6 socket will succeed. +has_ipv6 = _try_ipv6_socket() + +def create_socket(): + """Create a TCP socket with or without IPv6 depending on system support""" + if has_ipv6: + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + # Explicitly configure socket to work for both IPv4 and IPv6 + sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + else: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + return sock + +def format_hostname(hostname): + """Format hostname for display.""" + if (has_ipv6 and re.match('\d+.\d+.\d+.\d+', hostname) is not None): + hostname = '::ffff:%s' % hostname + return hostname diff --git a/tests/frontends/mpd/server_test.py b/tests/frontends/mpd/server_test.py index 32e90450..76bf9e33 100644 --- a/tests/frontends/mpd/server_test.py +++ b/tests/frontends/mpd/server_test.py @@ -5,29 +5,6 @@ from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import server from mopidy.mixers.dummy import DummyMixer -class MpdServerTest(unittest.TestCase): - def setUp(self): - 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.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.has_ipv6 = False - self.assertEquals(self.server._format_hostname('0.0.0.0'), '0.0.0.0') - class MpdSessionTest(unittest.TestCase): def setUp(self): self.backend = DummyBackend.start().proxy() diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py new file mode 100644 index 00000000..6217e910 --- /dev/null +++ b/tests/utils/network_test.py @@ -0,0 +1,19 @@ +import unittest + +from mopidy.utils import network + +class FormatHostnameTest(unittest.TestCase): + def setUp(self): + self.has_ipv6 = network.has_ipv6 + + def tearDown(self): + network.has_ipv6 = self.has_ipv6 + + def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self): + network.has_ipv6 = True + self.assertEqual(network.format_hostname('0.0.0.0'), '::ffff:0.0.0.0') + self.assertEqual(network.format_hostname('1.0.0.1'), '::ffff:1.0.0.1') + + def test_format_hostname_does_nothing_when_only_ipv4_available(self): + network.has_ipv6 = False + self.assertEquals(network.format_hostname('0.0.0.0'), '0.0.0.0') From ea9159a9babc17da7505ba334b0b71cd4c45a28c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 7 Jun 2011 15:23:33 +0200 Subject: [PATCH 3/8] Add test for try_ipv6_socket --- tests/utils/network_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index 6217e910..5a753f6e 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -1,3 +1,4 @@ +import mock import unittest from mopidy.utils import network @@ -17,3 +18,20 @@ class FormatHostnameTest(unittest.TestCase): def test_format_hostname_does_nothing_when_only_ipv4_available(self): network.has_ipv6 = False self.assertEquals(network.format_hostname('0.0.0.0'), '0.0.0.0') + +class TryIPv6SocketTest(unittest.TestCase): + @mock.patch('socket.has_ipv6', False) + def test_system_that_claims_no_ipv6_support(self): + self.assertFalse(network._try_ipv6_socket()) + + @mock.patch('socket.has_ipv6', True) + @mock.patch('socket.socket') + def test_system_with_broken_ipv6(self, socket_mock): + socket_mock.side_effect = IOError() + self.assertFalse(network._try_ipv6_socket()) + + @mock.patch('socket.has_ipv6', True) + @mock.patch('socket.socket') + def test_with_working_ipv6(self, socket_mock): + socket_mock.return_value = mock.Mock() + self.assertTrue(network._try_ipv6_socket()) From 14a9a3fb6684f3eab628d59adacbff0a154716e7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 7 Jun 2011 15:25:48 +0200 Subject: [PATCH 4/8] Use mocking for network.has_ipv6 monkey patching --- tests/utils/network_test.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index 5a753f6e..2f0cda66 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -4,17 +4,13 @@ import unittest from mopidy.utils import network class FormatHostnameTest(unittest.TestCase): - def setUp(self): - self.has_ipv6 = network.has_ipv6 - - def tearDown(self): - network.has_ipv6 = self.has_ipv6 - + @mock.patch('mopidy.utils.network.has_ipv6', True) def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self): network.has_ipv6 = True self.assertEqual(network.format_hostname('0.0.0.0'), '::ffff:0.0.0.0') self.assertEqual(network.format_hostname('1.0.0.1'), '::ffff:1.0.0.1') + @mock.patch('mopidy.utils.network.has_ipv6', False) def test_format_hostname_does_nothing_when_only_ipv4_available(self): network.has_ipv6 = False self.assertEquals(network.format_hostname('0.0.0.0'), '0.0.0.0') From cf3b6dcb2bdf4bcea2ce5b5f375c6f17b0601141 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 7 Jun 2011 15:45:52 +0200 Subject: [PATCH 5/8] Add create socket test --- tests/utils/network_test.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index 2f0cda66..66229036 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -1,8 +1,11 @@ import mock +import socket import unittest from mopidy.utils import network +from tests import SkipTest + class FormatHostnameTest(unittest.TestCase): @mock.patch('mopidy.utils.network.has_ipv6', True) def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self): @@ -15,6 +18,7 @@ class FormatHostnameTest(unittest.TestCase): network.has_ipv6 = False self.assertEquals(network.format_hostname('0.0.0.0'), '0.0.0.0') + class TryIPv6SocketTest(unittest.TestCase): @mock.patch('socket.has_ipv6', False) def test_system_that_claims_no_ipv6_support(self): @@ -31,3 +35,23 @@ class TryIPv6SocketTest(unittest.TestCase): def test_with_working_ipv6(self, socket_mock): socket_mock.return_value = mock.Mock() self.assertTrue(network._try_ipv6_socket()) + + +class CreateSocketTest(unittest.TestCase): + @mock.patch('mopidy.utils.network.has_ipv6', False) + @mock.patch('socket.socket') + def test_ipv4_socket(self, socket_mock): + network.create_socket() + self.assertEqual(socket_mock.call_args[0], + (socket.AF_INET, socket.SOCK_STREAM)) + + @mock.patch('mopidy.utils.network.has_ipv6', True) + @mock.patch('socket.socket') + def test_ipv6_socket(self, socket_mock): + network.create_socket() + self.assertEqual(socket_mock.call_args[0], + (socket.AF_INET6, socket.SOCK_STREAM)) + + @SkipTest + def test_ipv6_only_is_set(self): + pass From 20d3b48bb5ffb8d2fbbc2ee7c4b14b3ecc751d65 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Jun 2011 16:02:29 +0200 Subject: [PATCH 6/8] Update changelog with fix for GH-59 (fixes: #59) --- docs/changes.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index b4d56711..b13dad26 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -29,6 +29,13 @@ No description yet. - Replace not decodable characters returned from Spotify instead of throwing an exception, as we won't try to figure out the encoding of non-UTF-8-data. +- Spotify backend: + + - Thanks to Antoine Pierlot-Garcin's recent work on updating and improving + pyspotify, stored playlists will again load when Mopidy starts. The + workaround of searching and reconnecting to make the playlists appear are + no longer necessary. (Fixes: :issue:`59`) + - MPD frontend: - Refactoring and cleanup. Most notably, all request handlers now get an From f35eb4aa9b45d93e134e384426ea6cfd8ce74c2a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Jun 2011 16:04:26 +0200 Subject: [PATCH 7/8] Require libspotify 0.0.8 and pyspotify 1.2 --- docs/changes.rst | 8 ++++++++ docs/installation/libspotify.rst | 28 ++++++++++++---------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index b13dad26..a3a8f1ce 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -10,8 +10,16 @@ v0.5.0 (in development) No description yet. +Please note that 0.5.0 requires some updated dependencies, as listed under +*Important changes* below. + **Important changes** +- If you use the Spotify backend, you *must* upgrade to libspotify 0.0.8 and + pyspotify 1.2. If you install from APT, libspotify and pyspotify will + automatically be upgraded. If you are not installing from APT, follow the + instructions at :doc:`/installation/libspotify/`. + - Mopidy now supports running with 1-n outputs at the same time. This feature was mainly added to facilitate Shoutcast support, which Mopidy has also gained. In its current state outputs can not be toggled during runtime. diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index ca0ad87d..2728be94 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -4,8 +4,8 @@ libspotify installation Mopidy uses `libspotify `_ for playing music from -the Spotify music service. To use :mod:`mopidy.backends.libspotify` you must -install libspotify and `pyspotify `_. +the Spotify music service. To use :mod:`mopidy.backends.spotify` you must +install libspotify and `pyspotify `_. .. note:: @@ -30,7 +30,7 @@ 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 libspotify7 + sudo apt-get install libspotify8 When libspotify has been installed, continue with :ref:`pyspotify_installation`. @@ -39,14 +39,14 @@ When libspotify has been installed, continue with On Linux from source -------------------- -Download and install libspotify 0.0.7 for your OS and CPU architecture from +Download and install libspotify 0.0.8 for your OS and CPU architecture from https://developer.spotify.com/en/libspotify/. For 64-bit Linux the process is as follows:: - wget http://developer.spotify.com/download/libspotify/libspotify-0.0.7-linux6-x86_64.tar.gz - tar zxfv libspotify-0.0.7-linux6-x86_64.tar.gz - cd libspotify-0.0.7-linux6-x86_64/ + 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 + cd libspotify-0.0.8-linux6-x86_64/ sudo make install prefix=/usr/local sudo ldconfig @@ -103,14 +103,10 @@ Debian/Ubuntu systems run:: On OS X no additional dependencies are needed. -Get the pyspotify code, and install it:: +Then get, build, and install the latest releast of pyspotify using ``pip``:: - wget --no-check-certificate -O pyspotify.tar.gz https://github.com/mopidy/pyspotify/tarball/mopidy - tar zxfv pyspotify.tar.gz - cd pyspotify/ - sudo python setup.py install + sudo pip install -U pyspotify -It is important that you install pyspotify from the ``mopidy`` branch of the -``mopidy/pyspotify`` repository, as the upstream repository at -``winjer/pyspotify`` is not updated with changes needed to support e.g. -libspotify 0.0.7 and high bitrate audio. +Or using the older ``easy_install``:: + + sudo easy_install pyspotify From 8b9fb90449a74db0fa719f34c3ac7e5fe013a99a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 7 Jun 2011 16:11:34 +0200 Subject: [PATCH 8/8] Fix logging of mopidy server port --- mopidy/frontends/mpd/server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 68796c49..927e2a00 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -27,9 +27,7 @@ class MpdServer(asyncore.dispatcher): logger.debug(u'MPD server is binding to [%s]:%s', hostname, port) self.bind((hostname, port)) self.listen(1) - logger.info(u'MPD server running at [%s]:%s', - self._format_hostname(settings.MPD_SERVER_HOSTNAME), - settings.MPD_SERVER_PORT) + logger.info(u'MPD server running at [%s]:%s', hostname, port) except IOError, e: logger.error(u'MPD server startup failed: %s' % str(e).decode('utf-8'))