Merge branch 'develop' into feature/switch-to-gst-mixers

Conflicts:
	mopidy/gstreamer.py
This commit is contained in:
Thomas Adamcik 2012-09-03 22:54:22 +02:00
commit 114bc10ae8
9 changed files with 421 additions and 42 deletions

View File

@ -18,11 +18,19 @@ v0.8 (in development)
Track position and CPID was intermixed, so it would cause a crash if a CPID
matching the track position didn't exist. (Fixes: :issue:`162`)
- Removed most traces of multiple outputs support. Having this feature
currently seems to be more trouble than what it is worth.
:attr:`mopidy.settings.OUTPUTS` setting is no longer supported, and has been
replaced with :attr:`mopidy.settings.OUTPUT` which is a GStreamer
bin described in the same format as ``gst-launch`` expects. Default value is
- Added :option:`--list-deps` option to :cmd:`mopidy` command that lists
required and optional dependencies, their current versions, and some other
information useful for debugging. (Fixes: :issue:`74`)
- When unknown settings are encountered, we now check if it's similar to a
known setting, and suggests to the user what we think the setting should have
been.
- Removed multiple outputs support. Having this feature currently seems to be
more trouble than what it is worth. The :attr:`mopidy.settings.OUTPUTS`
setting is no longer supported, and has been replaced with
:attr:`mopidy.settings.OUTPUT` which is a GStreamer bin description string in
the same format as ``gst-launch`` expects. Default value is
``autoaudiosink``.

View File

@ -157,13 +157,13 @@ server simultaneously. To use the SHOUTcast output, do the following:
#. Install, configure and start the Icecast server. It can be found in the
``icecast2`` package in Debian/Ubuntu.
#. Set :attr:`mopidy.settings.OUTPUT` to ``lame ! shout2send`` (an Ogg Vorbis
encoder could be used instead of lame).
#. Set :attr:`mopidy.settings.OUTPUT` to ``lame ! shout2send``. An Ogg Vorbis
encoder could be used instead of the lame MP3 encoder.
#. You might also need to change the ``shout2send`` default settings, run
``gst-inspect-0.10 shout2send`` to see the available settings. Most likely
you want to change ``ip``, ``username``, ``password`` and ``mount``. For
example, to set the password use:
you want to change ``ip``, ``username``, ``password``, and ``mount``. For
example, to set the username and password, use:
``lame ! shout2send username="foobar" password="s3cret"``.
Other advanced setups are also possible for outputs. Basically anything you can

View File

@ -22,6 +22,7 @@ from mopidy import (get_version, settings, OptionalDependencyError,
SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE)
from mopidy.gstreamer import GStreamer
from mopidy.utils import get_class
from mopidy.utils.deps import list_deps_optparse_callback
from mopidy.utils.log import setup_logging
from mopidy.utils.path import get_or_create_folder, get_or_create_file
from mopidy.utils.process import (exit_handler, stop_remaining_actors,
@ -77,6 +78,9 @@ def parse_options():
parser.add_option('--list-settings',
action='callback', callback=list_settings_optparse_callback,
help='list current settings')
parser.add_option('--list-deps',
action='callback', callback=list_deps_optparse_callback,
help='list dependencies and their versions')
return parser.parse_args(args=mopidy_args)[0]
def check_old_folders():

View File

@ -196,7 +196,7 @@ def _list_build_query(field, mpd_query):
if error.message == 'No closing quotation':
raise MpdArgError(u'Invalid unquoted character', command=u'list')
else:
raise error
raise
tokens = [t.decode('utf-8') for t in tokens]
if len(tokens) == 1:
if field == u'album':

View File

@ -151,7 +151,6 @@ class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer):
def set_record(self, track, record):
pass
gobject.type_register(FakeMixer)
gst.element_register (FakeMixer, 'fakemixer', gst.RANK_MARGINAL)

193
mopidy/utils/deps.py Normal file
View File

@ -0,0 +1,193 @@
import os
import platform
import sys
import pygst
pygst.require('0.10')
import gst
import pykka
from mopidy.utils.log import indent
def list_deps_optparse_callback(*args):
"""
Prints a list of all dependencies.
Called by optparse when Mopidy is run with the :option:`--list-deps`
option.
"""
print format_dependency_list()
sys.exit(0)
def format_dependency_list(adapters=None):
if adapters is None:
adapters = [
platform_info,
python_info,
gstreamer_info,
pykka_info,
pyspotify_info,
pylast_info,
dbus_info,
serial_info,
]
lines = []
for adapter in adapters:
dep_info = adapter()
lines.append('%(name)s: %(version)s' % {
'name': dep_info['name'],
'version': dep_info.get('version', 'not found'),
})
if 'path' in dep_info:
lines.append(' Imported from: %s' % (
os.path.dirname(dep_info['path'])))
if 'other' in dep_info:
lines.append(' Other: %s' % (
indent(dep_info['other'])),)
return '\n'.join(lines)
def platform_info():
return {
'name': 'Platform',
'version': platform.platform(),
}
def python_info():
return {
'name': 'Python',
'version': '%s %s' % (platform.python_implementation(),
platform.python_version()),
'path': platform.__file__,
}
def gstreamer_info():
other = []
other.append('Python wrapper: gst-python %s' % (
'.'.join(map(str, gst.get_pygst_version()))))
other.append('Relevant elements:')
for name, status in _gstreamer_check_elements():
other.append(' %s: %s' % (name, 'OK' if status else 'not found'))
return {
'name': 'GStreamer',
'version': '.'.join(map(str, gst.get_gst_version())),
'path': gst.__file__,
'other': '\n'.join(other),
}
def _gstreamer_check_elements():
elements_to_check = [
# Core playback
'uridecodebin',
# External HTTP streams
'souphttpsrc',
# Spotify
'appsrc',
# Mixers and sinks
'alsamixer',
'alsasink',
'ossmixer',
'osssink',
'oss4mixer',
'oss4sink',
'pulsemixer',
'pulsesink',
# MP3 encoding and decoding
'mp3parse',
'mad',
'id3demux',
'id3v2mux',
'lame',
# Ogg Vorbis encoding and decoding
'vorbisdec',
'vorbisenc',
'vorbisparse',
'oggdemux',
'oggmux',
'oggparse',
# Flac decoding
'flacdec',
'flacparse',
# Shoutcast output
'shout2send',
]
known_elements = [factory.get_name() for factory in
gst.registry_get_default().get_feature_list(gst.TYPE_ELEMENT_FACTORY)]
return [(element, element in known_elements) for element in elements_to_check]
def pykka_info():
if hasattr(pykka, '__version__'):
# Pykka >= 0.14
version = pykka.__version__
else:
# Pykka < 0.14
version = pykka.get_version()
return {
'name': 'Pykka',
'version': version,
'path': pykka.__file__,
}
def pyspotify_info():
dep_info = {'name': 'pyspotify'}
try:
import spotify
if hasattr(spotify, '__version__'):
dep_info['version'] = spotify.__version__
else:
dep_info['version'] = '< 1.3'
dep_info['path'] = spotify.__file__
dep_info['other'] = 'Built for libspotify API version %d' % (
spotify.api_version,)
except ImportError:
pass
return dep_info
def pylast_info():
dep_info = {'name': 'pylast'}
try:
import pylast
dep_info['version'] = pylast.__version__
dep_info['path'] = pylast.__file__
except ImportError:
pass
return dep_info
def dbus_info():
dep_info = {'name': 'dbus-python'}
try:
import dbus
dep_info['version'] = dbus.__version__
dep_info['path'] = dbus.__file__
except ImportError:
pass
return dep_info
def serial_info():
dep_info = {'name': 'pyserial'}
try:
import serial
dep_info['version'] = serial.VERSION
dep_info['path'] = serial.__file__
except ImportError:
pass
return dep_info

View File

@ -12,6 +12,7 @@ from mopidy.utils.log import indent
logger = logging.getLogger('mopidy.utils.settings')
class SettingsProxy(object):
def __init__(self, default_settings_module):
self.default = self._get_settings_dict_from_module(
@ -101,7 +102,7 @@ def validate_settings(defaults, settings):
Checks the settings for both errors like misspellings and against a set of
rules for renamed settings, etc.
Returns of setting names with associated errors.
Returns mapping from setting names to associated errors.
:param defaults: Mopidy's default settings
:type defaults: dict
@ -116,9 +117,9 @@ def validate_settings(defaults, settings):
'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME',
'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT',
'FRONTEND': 'FRONTENDS',
'GSTREAMER_AUDIO_SINK': 'CUSTOM_OUTPUT',
'GSTREAMER_AUDIO_SINK': 'OUTPUT',
'LOCAL_MUSIC_FOLDER': 'LOCAL_MUSIC_PATH',
'LOCAL_OUTPUT_OVERRIDE': 'CUSTOM_OUTPUT',
'LOCAL_OUTPUT_OVERRIDE': 'OUTPUT',
'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH',
'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE',
'MIXER_ALSA_CONTROL': None,
@ -152,7 +153,7 @@ def validate_settings(defaults, settings):
elif setting == 'OUTPUTS':
errors[setting] = (
u'Deprecated setting, please change to OUTPUT. OUTPUT expectes '
u'a GStreamer bin describing your desired output.')
u'a GStreamer bin description string for your desired output.')
elif setting == 'SPOTIFY_BITRATE':
if value not in (96, 160, 320):
@ -166,11 +167,15 @@ def validate_settings(defaults, settings):
u'bin in OUTPUT.')
elif setting not in defaults:
errors[setting] = u'Unknown setting. Is it misspelled?'
continue
errors[setting] = u'Unknown setting.'
suggestion = did_you_mean(setting, defaults)
if suggestion:
errors[setting] += u' Did you mean %s?' % suggestion
return errors
def list_settings_optparse_callback(*args):
"""
Prints a list of all settings.
@ -182,6 +187,7 @@ def list_settings_optparse_callback(*args):
print format_settings_list(settings)
sys.exit(0)
def format_settings_list(settings):
errors = settings.get_errors()
lines = []
@ -196,8 +202,41 @@ def format_settings_list(settings):
lines.append(u' Error: %s' % errors[key])
return '\n'.join(lines)
def mask_value_if_secret(key, value):
if key.endswith('PASSWORD') and value:
return u'********'
else:
return value
def did_you_mean(setting, defaults):
"""Suggest most likely setting based on levenshtein."""
if not defaults:
return None
setting = setting.upper()
candidates = [(levenshtein(setting, d), d) for d in defaults]
candidates.sort()
if candidates[0][0] <= 3:
return candidates[0][1]
return None
def levenshtein(a, b, max=3):
"""Calculates the Levenshtein distance between a and b."""
n, m = len(a), len(b)
if n > m:
return levenshtein(b, a)
current = xrange(n+1)
for i in xrange(1, m+1):
previous, current = current, [i] + [0] * n
for j in xrange(1, n+1):
add, delete = previous[j] + 1, current[j-1] + 1
change = previous[j-1]
if a[j-1] != b[i-1]:
change += 1
current[j] = min(add, delete, change)
return current[n]

113
tests/utils/deps_test.py Normal file
View File

@ -0,0 +1,113 @@
import platform
import pygst
pygst.require('0.10')
import gst
import pykka
try:
import dbus
except ImportError:
dbus = False
try:
import pylast
except ImportError:
pylast = False
try:
import serial
except ImportError:
serial = False
try:
import spotify
except ImportError:
spotify = False
from mopidy.utils import deps
from tests import unittest
class DepsTest(unittest.TestCase):
def test_format_dependency_list(self):
adapters = [
lambda: dict(name='Python', version='FooPython 2.7.3'),
lambda: dict(name='Platform', version='Loonix 4.0.1'),
lambda: dict(name='Pykka', path='/foo/bar/baz.py', other='Quux')
]
result = deps.format_dependency_list(adapters)
self.assertIn('Python: FooPython 2.7.3', result)
self.assertIn('Platform: Loonix 4.0.1', result)
self.assertIn('Pykka: not found', result)
self.assertIn('Imported from: /foo/bar', result)
self.assertNotIn('/baz.py', result)
self.assertIn('Quux', result)
def test_platform_info(self):
result = deps.platform_info()
self.assertEquals('Platform', result['name'])
self.assertIn(platform.platform(), result['version'])
def test_python_info(self):
result = deps.python_info()
self.assertEquals('Python', result['name'])
self.assertIn(platform.python_implementation(), result['version'])
self.assertIn(platform.python_version(), result['version'])
self.assertIn('python', result['path'])
def test_gstreamer_info(self):
result = deps.gstreamer_info()
self.assertEquals('GStreamer', result['name'])
self.assertEquals('.'.join(map(str, gst.get_gst_version())), result['version'])
self.assertIn('gst', result['path'])
self.assertIn('Python wrapper: gst-python', result['other'])
self.assertIn('.'.join(map(str, gst.get_pygst_version())), result['other'])
self.assertIn('Relevant elements:', result['other'])
def test_pykka_info(self):
result = deps.pykka_info()
self.assertEquals('Pykka', result['name'])
self.assertEquals(pykka.__version__, result['version'])
self.assertIn('pykka', result['path'])
@unittest.skipUnless(spotify, 'pyspotify not found')
def test_pyspotify_info(self):
result = deps.pyspotify_info()
self.assertEquals('pyspotify', result['name'])
self.assertEquals(spotify.__version__, result['version'])
self.assertIn('spotify', result['path'])
self.assertIn('Built for libspotify API version', result['other'])
self.assertIn(str(spotify.api_version), result['other'])
@unittest.skipUnless(pylast, 'pylast not found')
def test_pylast_info(self):
result = deps.pylast_info()
self.assertEquals('pylast', result['name'])
self.assertEquals(pylast.__version__, result['version'])
self.assertIn('pylast', result['path'])
@unittest.skipUnless(dbus, 'dbus not found')
def test_dbus_info(self):
result = deps.dbus_info()
self.assertEquals('dbus-python', result['name'])
self.assertEquals(dbus.__version__, result['version'])
self.assertIn('dbus', result['path'])
@unittest.skipUnless(serial, 'serial not found')
def test_serial_info(self):
result = deps.serial_info()
self.assertEquals('pyserial', result['name'])
self.assertEquals(serial.VERSION, result['version'])
self.assertIn('serial', result['path'])

View File

@ -1,8 +1,7 @@
import os
from mopidy import settings as default_settings_module, SettingsError
from mopidy.utils.settings import (format_settings_list, mask_value_if_secret,
SettingsProxy, validate_settings)
import mopidy
from mopidy.utils import settings as setting_utils
from tests import unittest
@ -16,29 +15,29 @@ class ValidateSettingsTest(unittest.TestCase):
}
def test_no_errors_yields_empty_dict(self):
result = validate_settings(self.defaults, {})
result = setting_utils.validate_settings(self.defaults, {})
self.assertEqual(result, {})
def test_unknown_setting_returns_error(self):
result = validate_settings(self.defaults,
result = setting_utils.validate_settings(self.defaults,
{'MPD_SERVER_HOSTNMAE': '127.0.0.1'})
self.assertEqual(result['MPD_SERVER_HOSTNMAE'],
u'Unknown setting. Is it misspelled?')
u'Unknown setting. Did you mean MPD_SERVER_HOSTNAME?')
def test_not_renamed_setting_returns_error(self):
result = validate_settings(self.defaults,
result = setting_utils.validate_settings(self.defaults,
{'SERVER_HOSTNAME': '127.0.0.1'})
self.assertEqual(result['SERVER_HOSTNAME'],
u'Deprecated setting. Use MPD_SERVER_HOSTNAME.')
def test_unneeded_settings_returns_error(self):
result = validate_settings(self.defaults,
result = setting_utils.validate_settings(self.defaults,
{'SPOTIFY_LIB_APPKEY': '/tmp/foo'})
self.assertEqual(result['SPOTIFY_LIB_APPKEY'],
u'Deprecated setting. It may be removed.')
def test_deprecated_setting_value_returns_error(self):
result = validate_settings(self.defaults,
result = setting_utils.validate_settings(self.defaults,
{'BACKENDS': ('mopidy.backends.despotify.DespotifyBackend',)})
self.assertEqual(result['BACKENDS'],
u'Deprecated setting value. ' +
@ -46,33 +45,33 @@ class ValidateSettingsTest(unittest.TestCase):
'available.')
def test_unavailable_bitrate_setting_returns_error(self):
result = validate_settings(self.defaults,
result = setting_utils.validate_settings(self.defaults,
{'SPOTIFY_BITRATE': 50})
self.assertEqual(result['SPOTIFY_BITRATE'],
u'Unavailable Spotify bitrate. ' +
u'Available bitrates are 96, 160, and 320.')
def test_two_errors_are_both_reported(self):
result = validate_settings(self.defaults,
result = setting_utils.validate_settings(self.defaults,
{'FOO': '', 'BAR': ''})
self.assertEqual(len(result), 2)
def test_masks_value_if_secret(self):
secret = mask_value_if_secret('SPOTIFY_PASSWORD', 'bar')
secret = setting_utils.mask_value_if_secret('SPOTIFY_PASSWORD', 'bar')
self.assertEqual(u'********', secret)
def test_does_not_mask_value_if_not_secret(self):
not_secret = mask_value_if_secret('SPOTIFY_USERNAME', 'foo')
not_secret = setting_utils.mask_value_if_secret('SPOTIFY_USERNAME', 'foo')
self.assertEqual('foo', not_secret)
def test_does_not_mask_value_if_none(self):
not_secret = mask_value_if_secret('SPOTIFY_USERNAME', None)
not_secret = setting_utils.mask_value_if_secret('SPOTIFY_USERNAME', None)
self.assertEqual(None, not_secret)
class SettingsProxyTest(unittest.TestCase):
def setUp(self):
self.settings = SettingsProxy(default_settings_module)
self.settings = setting_utils.SettingsProxy(mopidy.settings)
self.settings.local.clear()
def test_set_and_get_attr(self):
@ -83,7 +82,7 @@ class SettingsProxyTest(unittest.TestCase):
try:
_ = self.settings.TEST
self.fail(u'Should raise exception')
except SettingsError as e:
except mopidy.SettingsError as e:
self.assertEqual(u'Setting "TEST" is not set.', e.message)
def test_getattr_raises_error_on_empty_setting(self):
@ -91,7 +90,7 @@ class SettingsProxyTest(unittest.TestCase):
try:
_ = self.settings.TEST
self.fail(u'Should raise exception')
except SettingsError as e:
except mopidy.SettingsError as e:
self.assertEqual(u'Setting "TEST" is empty.', e.message)
def test_getattr_does_not_raise_error_if_setting_is_false(self):
@ -177,44 +176,68 @@ class SettingsProxyTest(unittest.TestCase):
class FormatSettingListTest(unittest.TestCase):
def setUp(self):
self.settings = SettingsProxy(default_settings_module)
self.settings = setting_utils.SettingsProxy(mopidy.settings)
def test_contains_the_setting_name(self):
self.settings.TEST = u'test'
result = format_settings_list(self.settings)
result = setting_utils.format_settings_list(self.settings)
self.assert_('TEST:' in result, result)
def test_repr_of_a_string_value(self):
self.settings.TEST = u'test'
result = format_settings_list(self.settings)
result = setting_utils.format_settings_list(self.settings)
self.assert_("TEST: u'test'" in result, result)
def test_repr_of_an_int_value(self):
self.settings.TEST = 123
result = format_settings_list(self.settings)
result = setting_utils.format_settings_list(self.settings)
self.assert_("TEST: 123" in result, result)
def test_repr_of_a_tuple_value(self):
self.settings.TEST = (123, u'abc')
result = format_settings_list(self.settings)
result = setting_utils.format_settings_list(self.settings)
self.assert_("TEST: (123, u'abc')" in result, result)
def test_passwords_are_masked(self):
self.settings.TEST_PASSWORD = u'secret'
result = format_settings_list(self.settings)
result = setting_utils.format_settings_list(self.settings)
self.assert_("TEST_PASSWORD: u'secret'" not in result, result)
self.assert_("TEST_PASSWORD: u'********'" in result, result)
def test_short_values_are_not_pretty_printed(self):
self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend',)
result = format_settings_list(self.settings)
result = setting_utils.format_settings_list(self.settings)
self.assert_("FRONTEND: (u'mopidy.frontends.mpd.MpdFrontend',)" in result,
result)
def test_long_values_are_pretty_printed(self):
self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend',
u'mopidy.frontends.lastfm.LastfmFrontend')
result = format_settings_list(self.settings)
result = setting_utils.format_settings_list(self.settings)
self.assert_("""FRONTEND:
(u'mopidy.frontends.mpd.MpdFrontend',
u'mopidy.frontends.lastfm.LastfmFrontend')""" in result, result)
class DidYouMeanTest(unittest.TestCase):
def testSuggestoins(self):
defaults = {
'MPD_SERVER_HOSTNAME': '::',
'MPD_SERVER_PORT': 6600,
'SPOTIFY_BITRATE': 160,
}
suggestion = setting_utils.did_you_mean('spotify_bitrate', defaults)
self.assertEqual(suggestion, 'SPOTIFY_BITRATE')
suggestion = setting_utils.did_you_mean('SPOTIFY_BITROTE', defaults)
self.assertEqual(suggestion, 'SPOTIFY_BITRATE')
suggestion = setting_utils.did_you_mean('SPITIFY_BITROT', defaults)
self.assertEqual(suggestion, 'SPOTIFY_BITRATE')
suggestion = setting_utils.did_you_mean('SPTIFY_BITROT', defaults)
self.assertEqual(suggestion, 'SPOTIFY_BITRATE')
suggestion = setting_utils.did_you_mean('SPTIFY_BITRO', defaults)
self.assertEqual(suggestion, None)