210 lines
6.6 KiB
Python
210 lines
6.6 KiB
Python
# Absolute import needed to import ~/.config/mopidy/settings.py and not
|
|
# ourselves
|
|
from __future__ import absolute_import, unicode_literals
|
|
|
|
import copy
|
|
import getpass
|
|
import logging
|
|
import os
|
|
import pprint
|
|
import sys
|
|
|
|
from mopidy import exceptions
|
|
from mopidy.utils import formatting, path
|
|
|
|
logger = logging.getLogger('mopidy.utils.settings')
|
|
|
|
|
|
class SettingsProxy(object):
|
|
def __init__(self, default_settings_module):
|
|
self.default = self._get_settings_dict_from_module(
|
|
default_settings_module)
|
|
self.local = self._get_local_settings()
|
|
self.runtime = {}
|
|
|
|
def _get_local_settings(self):
|
|
if not os.path.isfile(path.SETTINGS_FILE):
|
|
return {}
|
|
sys.path.insert(0, path.SETTINGS_PATH)
|
|
# pylint: disable = F0401
|
|
import settings as local_settings_module
|
|
# pylint: enable = F0401
|
|
return self._get_settings_dict_from_module(local_settings_module)
|
|
|
|
def _get_settings_dict_from_module(self, module):
|
|
settings = filter(
|
|
lambda (key, value): self._is_setting(key),
|
|
module.__dict__.iteritems())
|
|
return dict(settings)
|
|
|
|
def _is_setting(self, name):
|
|
return name.isupper()
|
|
|
|
@property
|
|
def current(self):
|
|
current = copy.copy(self.default)
|
|
current.update(self.local)
|
|
current.update(self.runtime)
|
|
return current
|
|
|
|
def __getattr__(self, attr):
|
|
if not self._is_setting(attr):
|
|
return
|
|
|
|
current = self.current # bind locally to avoid copying+updates
|
|
if attr not in current:
|
|
raise exceptions.SettingsError('Setting "%s" is not set.' % attr)
|
|
|
|
value = current[attr]
|
|
if isinstance(value, basestring) and len(value) == 0:
|
|
raise exceptions.SettingsError('Setting "%s" is empty.' % attr)
|
|
if not value:
|
|
return value
|
|
if attr.endswith('_PATH') or attr.endswith('_FILE'):
|
|
value = path.expand_path(value)
|
|
return value
|
|
|
|
def __setattr__(self, attr, value):
|
|
if self._is_setting(attr):
|
|
self.runtime[attr] = value
|
|
else:
|
|
super(SettingsProxy, self).__setattr__(attr, value)
|
|
|
|
def validate(self):
|
|
if self.get_errors():
|
|
logger.error(
|
|
'Settings validation errors: %s',
|
|
formatting.indent(self.get_errors_as_string()))
|
|
raise exceptions.SettingsError('Settings validation failed.')
|
|
|
|
def _read_from_stdin(self, prompt):
|
|
if '_PASSWORD' in prompt:
|
|
return (
|
|
getpass.getpass(prompt)
|
|
.decode(sys.stdin.encoding, 'ignore'))
|
|
else:
|
|
sys.stdout.write(prompt)
|
|
return (
|
|
sys.stdin.readline().strip()
|
|
.decode(sys.stdin.encoding, 'ignore'))
|
|
|
|
def get_errors(self):
|
|
return validate_settings(self.default, self.local)
|
|
|
|
def get_errors_as_string(self):
|
|
lines = []
|
|
for (setting, error) in self.get_errors().iteritems():
|
|
lines.append('%s: %s' % (setting, error))
|
|
return '\n'.join(lines)
|
|
|
|
|
|
def validate_settings(defaults, settings):
|
|
"""
|
|
Checks the settings for both errors like misspellings and against a set of
|
|
rules for renamed settings, etc.
|
|
|
|
Returns mapping from setting names to associated errors.
|
|
|
|
:param defaults: Mopidy's default settings
|
|
:type defaults: dict
|
|
:param settings: the user's local settings
|
|
:type settings: dict
|
|
:rtype: dict
|
|
"""
|
|
errors = {}
|
|
|
|
changed = {
|
|
'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME',
|
|
'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT',
|
|
'GSTREAMER_AUDIO_SINK': 'OUTPUT',
|
|
'LOCAL_MUSIC_FOLDER': 'LOCAL_MUSIC_PATH',
|
|
'LOCAL_OUTPUT_OVERRIDE': 'OUTPUT',
|
|
'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH',
|
|
'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE',
|
|
'MIXER_ALSA_CONTROL': None,
|
|
'MIXER_EXT_PORT': None,
|
|
'MIXER_EXT_SPEAKERS_A': None,
|
|
'MIXER_EXT_SPEAKERS_B': None,
|
|
'MIXER_MAX_VOLUME': None,
|
|
'SERVER': None,
|
|
'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME',
|
|
'SERVER_PORT': 'MPD_SERVER_PORT',
|
|
'SPOTIFY_HIGH_BITRATE': 'SPOTIFY_BITRATE',
|
|
'SPOTIFY_LIB_APPKEY': None,
|
|
'SPOTIFY_LIB_CACHE': 'SPOTIFY_CACHE_PATH',
|
|
}
|
|
|
|
must_be_iterable = [
|
|
'STREAM_PROTOCOLS',
|
|
]
|
|
|
|
for setting, value in settings.iteritems():
|
|
if setting in changed:
|
|
if changed[setting] is None:
|
|
errors[setting] = 'Deprecated setting. It may be removed.'
|
|
else:
|
|
errors[setting] = 'Deprecated setting. Use %s.' % (
|
|
changed[setting],)
|
|
|
|
elif setting == 'OUTPUTS':
|
|
errors[setting] = (
|
|
'Deprecated setting, please change to OUTPUT. OUTPUT expects '
|
|
'a GStreamer bin description string for your desired output.')
|
|
|
|
elif setting == 'SPOTIFY_BITRATE':
|
|
if value not in (96, 160, 320):
|
|
errors[setting] = (
|
|
'Unavailable Spotify bitrate. Available bitrates are 96, '
|
|
'160, and 320.')
|
|
|
|
elif setting.startswith('SHOUTCAST_OUTPUT_'):
|
|
errors[setting] = (
|
|
'Deprecated setting, please set the value via the GStreamer '
|
|
'bin in OUTPUT.')
|
|
|
|
elif setting in must_be_iterable and not hasattr(value, '__iter__'):
|
|
errors[setting] = (
|
|
'Must be a tuple. '
|
|
"Remember the comma after single values: (u'value',)")
|
|
|
|
elif setting not in defaults and not setting.startswith('CUSTOM_'):
|
|
errors[setting] = 'Unknown setting.'
|
|
suggestion = did_you_mean(setting, defaults)
|
|
|
|
if suggestion:
|
|
errors[setting] += ' Did you mean %s?' % suggestion
|
|
|
|
return errors
|
|
|
|
|
|
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):
|
|
"""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]
|