Merge branch 'develop' into feature/mpris-frontend
This commit is contained in:
commit
ba1ab11b7b
@ -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
|
||||
|
||||
@ -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)
|
||||
==================
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"""
|
||||
|
||||
@ -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 '
|
||||
|
||||
@ -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)')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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('')
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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())
|
||||
|
||||
Loading…
Reference in New Issue
Block a user