Merge branch 'develop' into feature/use-new-config

Conflicts:
	mopidy/frontends/scrobbler/actor.py
This commit is contained in:
Stein Magnus Jodal 2013-04-06 01:50:15 +02:00
commit 81b1e10c1a
15 changed files with 89 additions and 93 deletions

View File

@ -45,6 +45,6 @@ Frontend implementations
========================
* :mod:`mopidy.frontends.http`
* :mod:`mopidy.frontends.lastfm`
* :mod:`mopidy.frontends.mpd`
* :mod:`mopidy.frontends.mpris`
* :mod:`mopidy.frontends.scrobbler`

View File

@ -1,6 +0,0 @@
***************************************************
:mod:`mopidy.frontends.lastfm` -- Last.fm Scrobbler
***************************************************
.. automodule:: mopidy.frontends.lastfm
:synopsis: Last.fm scrobbler frontend

View File

@ -0,0 +1,6 @@
**********************************************
:mod:`mopidy.frontends.scrobble` -- Scrobbler
**********************************************
.. automodule:: mopidy.frontends.scrobbler
:synopsis: Music scrobbler frontend

View File

@ -106,8 +106,8 @@ Scrobbling tracks to Last.fm
If you want to submit the tracks you are playing to your `Last.fm
<http://www.last.fm/>`_ profile, make sure you've installed the dependencies
found at :mod:`mopidy.frontends.lastfm` and add the following to your settings
file::
found at :mod:`mopidy.frontends.scrobbler` and add the following to your
settings file::
LASTFM_USERNAME = u'myusername'
LASTFM_PASSWORD = u'mysecret'

View File

@ -26,11 +26,6 @@ timeout = 10
# Path to the Spotify data cache. Cannot be shared with other Spotify apps
cache_path = $XDG_CACHE_DIR/mopidy/spotify
# Connect to Spotify through a proxy
proxy_hostname =
proxy_username =
proxy_password =
"""
__doc__ = """A backend for playing music from Spotify
@ -81,9 +76,6 @@ class Extension(ext.Extension):
schema['bitrate'] = config.Integer(choices=(96, 160, 320))
schema['timeout'] = config.Integer(minimum=0)
schema['cache_path'] = config.String()
schema['proxy_hostname'] = config.Hostname(optional=True)
schema['proxy_username'] = config.String(optional=True)
schema['proxy_password'] = config.String(optional=True, secret=True)
return schema
def validate_environment(self):

View File

@ -35,9 +35,9 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
PyspotifySessionManager.__init__(
self, config['spotify']['username'], config['spotify']['password'],
proxy=config['spotify']['proxy_hostname'],
proxy_username=config['spotify']['proxy_username'],
proxy_password=config['spotify']['proxy_password'])
proxy=config['proxy']['hostname'],
proxy_username=config['proxy']['username'],
proxy_password=config['proxy']['password'])
process.BaseThread.__init__(self)
self.name = 'SpotifyThread'

View File

@ -16,6 +16,11 @@ pykka = info
mixer = autoaudiomixer
mixer_track =
output = autoaudiosink
[proxy]
hostname =
username =
password =
"""
config_schemas = {} # TODO: use ordered dict?
@ -31,6 +36,11 @@ config_schemas['audio']['mixer'] = config.String()
config_schemas['audio']['mixer_track'] = config.String(optional=True)
config_schemas['audio']['output'] = config.String()
config_schemas['proxy'] = config.ConfigSchema()
config_schemas['proxy']['hostname'] = config.Hostname(optional=True)
config_schemas['proxy']['username'] = config.String(optional=True)
config_schemas['proxy']['password'] = config.String(optional=True, secret=True)
# NOTE: if multiple outputs ever comes something like LogLevelConfigSchema
#config_schemas['audio.outputs'] = config.AudioOutputConfigSchema()

View File

@ -6,7 +6,7 @@ from mopidy.utils import config, formatting
default_config = """
[lastfm]
[scrobbler]
# If the Last.fm extension should be enabled or not
enabled = true
@ -28,7 +28,7 @@ Frontend which scrobbles the music you play to your `Last.fm
**Dependencies**
.. literalinclude:: ../../../requirements/lastfm.txt
.. literalinclude:: ../../../requirements/scrobbler.txt
**Default config**
@ -44,8 +44,8 @@ The frontend is enabled by default if all dependencies are available.
class Extension(ext.Extension):
dist_name = 'Mopidy-Lastfm'
ext_name = 'lastfm'
dist_name = 'Mopidy-Scrobbler'
ext_name = 'scrobbler'
version = mopidy.__version__
def get_default_config(self):
@ -64,5 +64,5 @@ class Extension(ext.Extension):
raise exceptions.ExtensionError('pylast library not found', e)
def get_frontend_classes(self):
from .actor import LastfmFrontend
return [LastfmFrontend]
from .actor import ScrobblerFrontend
return [ScrobblerFrontend]

View File

@ -13,15 +13,15 @@ try:
except ImportError as import_error:
raise exceptions.OptionalDependencyError(import_error)
logger = logging.getLogger('mopidy.frontends.lastfm')
logger = logging.getLogger('mopidy.frontends.scrobbler')
API_KEY = '2236babefa8ebb3d93ea467560d00d04'
API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd'
class LastfmFrontend(pykka.ThreadingActor, CoreListener):
class ScrobblerFrontend(pykka.ThreadingActor, CoreListener):
def __init__(self, config, core):
super(LastfmFrontend, self).__init__()
super(ScrobblerFrontend, self).__init__()
self.config = config
self.lastfm = None
self.last_start_time = None

View File

@ -33,6 +33,39 @@ def validate_maximum(value, maximum):
raise ValueError('%r must be smaller than %r.' % (value, maximum))
# TODO: move this and levenshtein to a more appropriate class.
def did_you_mean(name, choices):
"""Suggest most likely setting based on levenshtein."""
if not choices:
return None
name = name.lower()
candidates = [(levenshtein(name, c), c) for c in choices]
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]
class ConfigValue(object):
"""Represents a config key's value and how to handle it.
@ -248,6 +281,9 @@ class ConfigSchema(object):
values[key] = self._schema[key].deserialize(value)
except KeyError: # not in our schema
errors[key] = 'unknown config key.'
suggestion = did_you_mean(key, self._schema.keys())
if suggestion:
errors[key] += ' Did you mean %s?' % suggestion
except ValueError as e: # deserialization failed
errors[key] = str(e)

View File

@ -169,41 +169,5 @@ def validate_settings(defaults, settings):
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]

View File

@ -29,7 +29,7 @@ setup(
],
extras_require={
b'spotify': ['pyspotify >= 1.9, < 1.11'],
b'lastfm': ['pylast >= 0.5.7'],
b'scrobbler': ['pylast >= 0.5.7'],
b'http': ['cherrypy >= 3.2.2', 'ws4py >= 0.2.3'],
b'external_mixers': ['pyserial'],
},
@ -46,7 +46,7 @@ setup(
],
b'mopidy.ext': [
'http = mopidy.frontends.http:Extension [http]',
'lastfm = mopidy.frontends.lastfm:Extension [lastfm]',
'scrobbler = mopidy.frontends.scrobbler:Extension [scrobbler]',
'local = mopidy.backends.local:Extension',
'mpd = mopidy.frontends.mpd:Extension',
'mpris = mopidy.frontends.mpris:Extension',

View File

@ -395,3 +395,22 @@ class LogLevelConfigSchemaTest(unittest.TestCase):
result = schema.format('levels', {'foo.bar': logging.DEBUG, 'baz': logging.INFO})
self.assertEqual('\n'.join(expected), result)
class DidYouMeanTest(unittest.TestCase):
def testSuggestoins(self):
choices = ('enabled', 'username', 'password', 'bitrate', 'timeout')
suggestion = config.did_you_mean('bitrate', choices)
self.assertEqual(suggestion, 'bitrate')
suggestion = config.did_you_mean('bitrote', choices)
self.assertEqual(suggestion, 'bitrate')
suggestion = config.did_you_mean('Bitrot', choices)
self.assertEqual(suggestion, 'bitrate')
suggestion = config.did_you_mean('BTROT', choices)
self.assertEqual(suggestion, 'bitrate')
suggestion = config.did_you_mean('btro', choices)
self.assertEqual(suggestion, None)

View File

@ -24,8 +24,7 @@ class ValidateSettingsTest(unittest.TestCase):
result = setting_utils.validate_settings(
self.defaults, {'MPD_SERVER_HOSTNMAE': '127.0.0.1'})
self.assertEqual(
result['MPD_SERVER_HOSTNMAE'],
'Unknown setting. Did you mean MPD_SERVER_HOSTNAME?')
result['MPD_SERVER_HOSTNMAE'], 'Unknown setting.')
def test_custom_settings_does_not_return_errors(self):
result = setting_utils.validate_settings(
@ -149,27 +148,3 @@ class SettingsProxyTest(unittest.TestCase):
def test_value_ending_in_path_can_be_none(self):
self.settings.TEST_PATH = None
self.assertEqual(self.settings.TEST_PATH, None)
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)