Merge branch 'develop' into feature/extension-registry
@ -4,6 +4,15 @@
|
||||
API reference
|
||||
*************
|
||||
|
||||
.. warning:: API stability
|
||||
|
||||
Only APIs documented here are public and open for use by Mopidy
|
||||
extensions. We will change these APIs, but will keep the changelog up to
|
||||
date with all breaking changes.
|
||||
|
||||
From Mopidy 1.0 and onwards, we intend to keep these APIs far more stable.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
@ -16,4 +25,5 @@ API reference
|
||||
commands
|
||||
ext
|
||||
config
|
||||
zeroconf
|
||||
http
|
||||
|
||||
11
docs/api/zeroconf.rst
Normal file
@ -0,0 +1,11 @@
|
||||
.. _zeroconf-api:
|
||||
|
||||
************
|
||||
Zeroconf API
|
||||
************
|
||||
|
||||
.. module:: mopidy.zeroconf
|
||||
:synopsis: Helper for publishing of services on Zeroconf
|
||||
|
||||
.. autoclass:: Zeroconf
|
||||
:members:
|
||||
|
Before Width: | Height: | Size: 519 KiB After Width: | Height: | Size: 519 KiB |
@ -18,7 +18,7 @@ See :ref:`http-api` for details on how to build your own web client.
|
||||
woutervanwijk/Mopidy-Webclient
|
||||
==============================
|
||||
|
||||
.. image:: /_static/woutervanwijk-mopidy-webclient.png
|
||||
.. image:: woutervanwijk-mopidy-webclient.png
|
||||
:width: 1275
|
||||
:height: 600
|
||||
|
||||
@ -36,7 +36,7 @@ Also the web client used for Wouter's popular `Pi Musicbox
|
||||
Mopidy Lux
|
||||
==========
|
||||
|
||||
.. image:: /_static/dz0ny-mopidy-lux.png
|
||||
.. image:: dz0ny-mopidy-lux.png
|
||||
:width: 1000
|
||||
:height: 645
|
||||
|
||||
@ -50,7 +50,7 @@ A Mopidy web client made with AngularJS by Janez Troha.
|
||||
Moped
|
||||
=====
|
||||
|
||||
.. image:: /_static/martijnboland-moped.png
|
||||
.. image:: martijnboland-moped.png
|
||||
:width: 720
|
||||
:height: 450
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
@ -51,7 +51,7 @@ ncmpcpp
|
||||
A console client that works well with Mopidy, and is regularly used by Mopidy
|
||||
developers.
|
||||
|
||||
.. image:: /_static/mpd-client-ncmpcpp.png
|
||||
.. image:: mpd-client-ncmpcpp.png
|
||||
:width: 575
|
||||
:height: 426
|
||||
|
||||
@ -84,7 +84,7 @@ GMPC
|
||||
`GMPC <http://gmpc.wikia.com>`_ is a graphical MPD client (GTK+) which works
|
||||
well with Mopidy.
|
||||
|
||||
.. image:: /_static/mpd-client-gmpc.png
|
||||
.. image:: mpd-client-gmpc.png
|
||||
:width: 1000
|
||||
:height: 565
|
||||
|
||||
@ -101,7 +101,7 @@ Sonata
|
||||
`Sonata <http://sonata.berlios.de/>`_ is a graphical MPD client (GTK+).
|
||||
It generally works well with Mopidy, except for search.
|
||||
|
||||
.. image:: /_static/mpd-client-sonata.png
|
||||
.. image:: mpd-client-sonata.png
|
||||
:width: 475
|
||||
:height: 424
|
||||
|
||||
@ -140,7 +140,7 @@ Test date:
|
||||
Tested version:
|
||||
1.03.1 (released 2012-10-16)
|
||||
|
||||
.. image:: /_static/mpd-client-mpdroid.jpg
|
||||
.. image:: mpd-client-mpdroid.jpg
|
||||
:width: 288
|
||||
:height: 512
|
||||
|
||||
@ -269,7 +269,7 @@ Test date:
|
||||
Tested version:
|
||||
1.7.1
|
||||
|
||||
.. image:: /_static/mpd-client-mpod.jpg
|
||||
.. image:: mpd-client-mpod.jpg
|
||||
:width: 320
|
||||
:height: 480
|
||||
|
||||
@ -297,7 +297,7 @@ Test date:
|
||||
Tested version:
|
||||
1.7.1
|
||||
|
||||
.. image:: /_static/mpd-client-mpad.jpg
|
||||
.. image:: mpd-client-mpad.jpg
|
||||
:width: 480
|
||||
:height: 360
|
||||
|
||||
@ -332,7 +332,7 @@ other web clients, see :ref:`http-clients`.
|
||||
Rompr
|
||||
-----
|
||||
|
||||
.. image:: /_static/rompr.png
|
||||
.. image:: rompr.png
|
||||
:width: 557
|
||||
:height: 600
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ sound menu in Ubuntu since 10.10 or 11.04. By default, it only includes the
|
||||
Rhytmbox music player, but many other players can integrate with the sound
|
||||
menu, including the official Spotify player and Mopidy.
|
||||
|
||||
.. image:: /_static/ubuntu-sound-menu.png
|
||||
.. image:: ubuntu-sound-menu.png
|
||||
:height: 480
|
||||
:width: 955
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 284 KiB After Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
@ -28,6 +28,12 @@ class Mock(object):
|
||||
def __getattr__(self, name):
|
||||
if name in ('__file__', '__path__'):
|
||||
return '/dev/null'
|
||||
elif name == 'get_system_config_dirs':
|
||||
# glib.get_system_config_dirs()
|
||||
return tuple
|
||||
elif name == 'get_user_config_dir':
|
||||
# glib.get_user_config_dir()
|
||||
return str
|
||||
elif (name[0] == name[0].upper()
|
||||
# gst.interfaces.MIXER_TRACK_*
|
||||
and not name.startswith('MIXER_TRACK_')
|
||||
|
||||
@ -27,48 +27,6 @@ code. So, if you're out of work, the code coverage and flake8 data at the CI
|
||||
server should give you a place to start.
|
||||
|
||||
|
||||
Protocol debugger
|
||||
=================
|
||||
|
||||
Since the main interface provided to Mopidy is through the MPD protocol, it is
|
||||
crucial that we try and stay in sync with protocol developments. In an attempt
|
||||
to make it easier to debug differences Mopidy and MPD protocol handling we have
|
||||
created ``tools/debug-proxy.py``.
|
||||
|
||||
This tool is proxy that sits in front of two MPD protocol aware servers and
|
||||
sends all requests to both, returning the primary response to the client and
|
||||
then printing any diff in the two responses.
|
||||
|
||||
Note that this tool depends on ``gevent`` unlike the rest of Mopidy at the time
|
||||
of writing. See :option:`tools/debug-proxy.py --help` for available options.
|
||||
Sample session::
|
||||
|
||||
[127.0.0.1]:59714
|
||||
listallinfo
|
||||
--- Reference response
|
||||
+++ Actual response
|
||||
@@ -1,16 +1,1 @@
|
||||
-file: uri1
|
||||
-Time: 4
|
||||
-Artist: artist1
|
||||
-Title: track1
|
||||
-Album: album1
|
||||
-file: uri2
|
||||
-Time: 4
|
||||
-Artist: artist2
|
||||
-Title: track2
|
||||
-Album: album2
|
||||
-file: uri3
|
||||
-Time: 4
|
||||
-Artist: artist3
|
||||
-Title: track3
|
||||
-Album: album3
|
||||
-OK
|
||||
+ACK [2@0] {listallinfo} incorrect arguments
|
||||
|
||||
To ensure that Mopidy and MPD have comparable state it is suggested you scan
|
||||
the same media directory with both servers.
|
||||
|
||||
Documentation writing
|
||||
=====================
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
@ -9,7 +9,7 @@ January 2013, Mopidy will run with Spotify support on both the armel
|
||||
(soft-float) and armhf (hard-float) architectures, which includes the Raspbian
|
||||
distribution.
|
||||
|
||||
.. image:: /_static/raspberry-pi-by-jwrodgers.jpg
|
||||
.. image:: raspberry-pi-by-jwrodgers.jpg
|
||||
:width: 640
|
||||
:height: 427
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
@ -117,7 +117,7 @@ def main():
|
||||
try:
|
||||
return args.command.run(args, proxied_config)
|
||||
except NotImplementedError:
|
||||
print root_cmd.format_help()
|
||||
print(root_cmd.format_help())
|
||||
return 1
|
||||
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
@ -112,7 +112,7 @@ class Command(object):
|
||||
|
||||
def exit(self, status_code=0, message=None, usage=None):
|
||||
"""Optionally print a message and exit."""
|
||||
print '\n\n'.join(m for m in (usage, message) if m)
|
||||
print('\n\n'.join(m for m in (usage, message) if m))
|
||||
sys.exit(status_code)
|
||||
|
||||
def format_usage(self, prog=None):
|
||||
@ -335,7 +335,7 @@ class ConfigCommand(Command):
|
||||
self.set(base_verbosity_level=-1)
|
||||
|
||||
def run(self, config, errors, extensions):
|
||||
print config_lib.format(config, extensions, errors)
|
||||
print(config_lib.format(config, extensions, errors))
|
||||
return 0
|
||||
|
||||
|
||||
@ -347,5 +347,5 @@ class DepsCommand(Command):
|
||||
self.set(base_verbosity_level=-1)
|
||||
|
||||
def run(self):
|
||||
print deps.format_dependency_list()
|
||||
print(deps.format_dependency_list())
|
||||
return 0
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import io
|
||||
import os.path
|
||||
@ -10,13 +10,13 @@ from mopidy.utils import path
|
||||
|
||||
def load():
|
||||
settings_file = path.expand_path(b'$XDG_CONFIG_DIR/mopidy/settings.py')
|
||||
print 'Checking %s' % settings_file
|
||||
print('Checking %s' % settings_file)
|
||||
|
||||
setting_globals = {}
|
||||
try:
|
||||
execfile(settings_file, setting_globals)
|
||||
except Exception as e:
|
||||
print 'Problem loading settings: %s' % e
|
||||
print('Problem loading settings: %s' % e)
|
||||
return setting_globals
|
||||
|
||||
|
||||
@ -106,20 +106,20 @@ def main():
|
||||
'spotify', 'scrobbler', 'mpd', 'mpris', 'local', 'stream', 'http']
|
||||
extensions = [e for e in ext.load_extensions() if e.ext_name in known]
|
||||
|
||||
print b'Converted config:\n'
|
||||
print config_lib.format(config, extensions)
|
||||
print(b'Converted config:\n')
|
||||
print(config_lib.format(config, extensions))
|
||||
|
||||
conf_file = path.expand_path(b'$XDG_CONFIG_DIR/mopidy/mopidy.conf')
|
||||
if os.path.exists(conf_file):
|
||||
print '%s exists, exiting.' % conf_file
|
||||
print('%s exists, exiting.' % conf_file)
|
||||
sys.exit(1)
|
||||
|
||||
print 'Write new config to %s? [yN]' % conf_file,
|
||||
print('Write new config to %s? [yN]' % conf_file, end=' ')
|
||||
if raw_input() != 'y':
|
||||
print 'Not saving, exiting.'
|
||||
print('Not saving, exiting.')
|
||||
sys.exit(0)
|
||||
|
||||
serialized_config = config_lib.format(config, extensions, display=False)
|
||||
with io.open(conf_file, 'wb') as filehandle:
|
||||
filehandle.write(serialized_config)
|
||||
print 'Done.'
|
||||
print('Done.')
|
||||
|
||||
@ -9,9 +9,8 @@ import pykka
|
||||
from ws4py.messaging import TextMessage
|
||||
from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
|
||||
|
||||
from mopidy import models
|
||||
from mopidy import models, zeroconf
|
||||
from mopidy.core import CoreListener
|
||||
from mopidy.utils import zeroconf
|
||||
from . import ws
|
||||
|
||||
|
||||
|
||||
@ -5,9 +5,10 @@ import sys
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import zeroconf
|
||||
from mopidy.core import CoreListener
|
||||
from mopidy.frontends.mpd import session
|
||||
from mopidy.utils import encoding, network, process, zeroconf
|
||||
from mopidy.utils import encoding, network, process
|
||||
|
||||
logger = logging.getLogger('mopidy.frontends.mpd')
|
||||
|
||||
|
||||
@ -136,6 +136,31 @@ def model_json_decoder(dct):
|
||||
return dct
|
||||
|
||||
|
||||
class Ref(ImmutableObject):
|
||||
"""
|
||||
Model to represent URI references with a human friendly name and type
|
||||
attached. This is intended for use a lightweight object "free" of metadata
|
||||
that can be passed around instead of using full blown models.
|
||||
|
||||
:param uri: object URI
|
||||
:type uri: string
|
||||
:param name: object name
|
||||
:type name: string
|
||||
:param type: object type
|
||||
:type name: string
|
||||
"""
|
||||
|
||||
#: The object URI. Read-only.
|
||||
uri = None
|
||||
|
||||
#: The object name. Read-only.
|
||||
name = None
|
||||
|
||||
#: The object type, e.g. "artist", "album", "track", "playlist",
|
||||
#: "directory". Read-only.
|
||||
type = None
|
||||
|
||||
|
||||
class Artist(ImmutableObject):
|
||||
"""
|
||||
:param uri: artist URI
|
||||
|
||||
@ -4,7 +4,7 @@ import logging
|
||||
import socket
|
||||
import string
|
||||
|
||||
logger = logging.getLogger('mopidy.utils.zeroconf')
|
||||
logger = logging.getLogger('mopidy.zeroconf')
|
||||
|
||||
try:
|
||||
import dbus
|
||||
@ -25,7 +25,20 @@ def _convert_text_to_dbus_bytes(text):
|
||||
|
||||
|
||||
class Zeroconf(object):
|
||||
"""Publish a network service with Zeroconf using Avahi."""
|
||||
"""Publish a network service with Zeroconf.
|
||||
|
||||
Currently, this only works on Linux using Avahi via D-Bus.
|
||||
|
||||
:param str name: human readable name of the service, e.g. 'MPD on neptune'
|
||||
:param int port: TCP port of the service, e.g. 6600
|
||||
:param str stype: service type, e.g. '_mpd._tcp'
|
||||
:param str domain: local network domain name, defaults to ''
|
||||
:param str host: interface to advertise the service on, defaults to all
|
||||
interfaces
|
||||
:param text: extra information depending on ``stype``, defaults to empty
|
||||
list
|
||||
:type text: list of str
|
||||
"""
|
||||
|
||||
def __init__(self, name, port, stype=None, domain=None,
|
||||
host=None, text=None):
|
||||
@ -44,6 +57,11 @@ class Zeroconf(object):
|
||||
hostname=self.host or socket.getfqdn(), port=self.port)
|
||||
|
||||
def publish(self):
|
||||
"""Publish the service.
|
||||
|
||||
Call when your service starts.
|
||||
"""
|
||||
|
||||
if _is_loopback_address(self.host):
|
||||
logger.info(
|
||||
'Zeroconf publish on loopback interface is not supported.')
|
||||
@ -83,6 +101,11 @@ class Zeroconf(object):
|
||||
return False
|
||||
|
||||
def unpublish(self):
|
||||
"""Unpublish the service.
|
||||
|
||||
Call when your service shuts down.
|
||||
"""
|
||||
|
||||
if self.group:
|
||||
try:
|
||||
self.group.Reset()
|
||||
@ -5,7 +5,7 @@ import json
|
||||
import unittest
|
||||
|
||||
from mopidy.models import (
|
||||
Artist, Album, TlTrack, Track, Playlist, SearchResult,
|
||||
Ref, Artist, Album, TlTrack, Track, Playlist, SearchResult,
|
||||
ModelJSONEncoder, model_json_decoder)
|
||||
|
||||
|
||||
@ -54,6 +54,40 @@ class GenericCopyTest(unittest.TestCase):
|
||||
self.assertRaises(TypeError, test)
|
||||
|
||||
|
||||
class RefTest(unittest.TestCase):
|
||||
def test_uri(self):
|
||||
uri = 'an_uri'
|
||||
ref = Ref(uri=uri)
|
||||
self.assertEqual(ref.uri, uri)
|
||||
self.assertRaises(AttributeError, setattr, ref, 'uri', None)
|
||||
|
||||
def test_name(self):
|
||||
name = 'a name'
|
||||
ref = Ref(name=name)
|
||||
self.assertEqual(ref.name, name)
|
||||
self.assertRaises(AttributeError, setattr, ref, 'name', None)
|
||||
|
||||
def test_invalid_kwarg(self):
|
||||
test = lambda: SearchResult(foo='baz')
|
||||
self.assertRaises(TypeError, test)
|
||||
|
||||
def test_repr_without_results(self):
|
||||
self.assertEquals(
|
||||
"Ref(name=u'foo', type=u'artist', uri=u'uri')",
|
||||
repr(Ref(uri='uri', name='foo', type='artist')))
|
||||
|
||||
def test_serialize_without_results(self):
|
||||
self.assertDictEqual(
|
||||
{'__model__': 'Ref', 'uri': 'uri'},
|
||||
Ref(uri='uri').serialize())
|
||||
|
||||
def test_to_json_and_back(self):
|
||||
ref1 = Ref(uri='uri')
|
||||
serialized = json.dumps(ref1, cls=ModelJSONEncoder)
|
||||
ref2 = json.loads(serialized, object_hook=model_json_decoder)
|
||||
self.assertEqual(ref1, ref2)
|
||||
|
||||
|
||||
class ArtistTest(unittest.TestCase):
|
||||
def test_uri(self):
|
||||
uri = 'an_uri'
|
||||
|
||||
@ -1,195 +0,0 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import argparse
|
||||
import difflib
|
||||
import sys
|
||||
|
||||
from gevent import select, server, socket
|
||||
|
||||
COLORS = ['\033[1;%dm' % (30 + i) for i in range(8)]
|
||||
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = COLORS
|
||||
RESET = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
|
||||
|
||||
def proxy(client, address, reference_address, actual_address):
|
||||
"""Main handler code that gets called for each connection."""
|
||||
client.setblocking(False)
|
||||
|
||||
reference = connect(reference_address)
|
||||
actual = connect(actual_address)
|
||||
|
||||
if reference and actual:
|
||||
loop(client, address, reference, actual)
|
||||
else:
|
||||
print 'Could not connect to one of the backends.'
|
||||
|
||||
for sock in (client, reference, actual):
|
||||
close(sock)
|
||||
|
||||
|
||||
def connect(address):
|
||||
"""Connect to given address and set socket non blocking."""
|
||||
try:
|
||||
sock = socket.socket()
|
||||
sock.connect(address)
|
||||
sock.setblocking(False)
|
||||
except socket.error:
|
||||
return None
|
||||
return sock
|
||||
|
||||
|
||||
def close(sock):
|
||||
"""Shutdown and close our sockets."""
|
||||
try:
|
||||
sock.shutdown(socket.SHUT_WR)
|
||||
sock.close()
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
|
||||
def loop(client, address, reference, actual):
|
||||
"""Loop that handles one MPD reqeust/response pair per iteration."""
|
||||
|
||||
# Consume banners from backends
|
||||
responses = dict()
|
||||
disconnected = read(
|
||||
[reference, actual], responses, find_response_end_token)
|
||||
diff(address, '', responses[reference], responses[actual])
|
||||
|
||||
# We lost a backend, might as well give up.
|
||||
if disconnected:
|
||||
return
|
||||
|
||||
client.sendall(responses[reference])
|
||||
|
||||
while True:
|
||||
responses = dict()
|
||||
|
||||
# Get the command from the client. Not sure how an if this will handle
|
||||
# client sending multiple commands currently :/
|
||||
disconnected = read([client], responses, find_request_end_token)
|
||||
|
||||
# We lost the client, might as well give up.
|
||||
if disconnected:
|
||||
return
|
||||
|
||||
# Send the entire command to both backends.
|
||||
reference.sendall(responses[client])
|
||||
actual.sendall(responses[client])
|
||||
|
||||
# Get the entire resonse from both backends.
|
||||
disconnected = read(
|
||||
[reference, actual], responses, find_response_end_token)
|
||||
|
||||
# Send the client the complete reference response
|
||||
client.sendall(responses[reference])
|
||||
|
||||
# Compare our responses
|
||||
diff(address,
|
||||
responses[client], responses[reference], responses[actual])
|
||||
|
||||
# Give up if we lost a backend.
|
||||
if disconnected:
|
||||
return
|
||||
|
||||
|
||||
def read(sockets, responses, find_end_token):
|
||||
"""Keep reading from sockets until they disconnet or we find our token."""
|
||||
|
||||
# This function doesn't go to well with idle when backends are out of sync.
|
||||
disconnected = False
|
||||
|
||||
for sock in sockets:
|
||||
responses.setdefault(sock, '')
|
||||
|
||||
while sockets:
|
||||
for sock in select.select(sockets, [], [])[0]:
|
||||
data = sock.recv(4096)
|
||||
responses[sock] += data
|
||||
|
||||
if find_end_token(responses[sock]):
|
||||
sockets.remove(sock)
|
||||
|
||||
if not data:
|
||||
sockets.remove(sock)
|
||||
disconnected = True
|
||||
|
||||
return disconnected
|
||||
|
||||
|
||||
def find_response_end_token(data):
|
||||
"""Find token that indicates the response is over."""
|
||||
for line in data.splitlines(True):
|
||||
if line.startswith(('OK', 'ACK')) and line.endswith('\n'):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def find_request_end_token(data):
|
||||
"""Find token that indicates that request is over."""
|
||||
lines = data.splitlines(True)
|
||||
if not lines:
|
||||
return False
|
||||
elif 'command_list_ok_begin' == lines[0].strip():
|
||||
return 'command_list_end' == lines[-1].strip()
|
||||
else:
|
||||
return lines[0].endswith('\n')
|
||||
|
||||
|
||||
def diff(address, command, reference_response, actual_response):
|
||||
"""Print command from client and a unified diff of the responses."""
|
||||
sys.stdout.write('[%s]:%s\n%s' % (address[0], address[1], command))
|
||||
for line in difflib.unified_diff(reference_response.splitlines(True),
|
||||
actual_response.splitlines(True),
|
||||
fromfile='Reference response',
|
||||
tofile='Actual response'):
|
||||
|
||||
if line.startswith('+') and not line.startswith('+++'):
|
||||
sys.stdout.write(GREEN)
|
||||
elif line.startswith('-') and not line.startswith('---'):
|
||||
sys.stdout.write(RED)
|
||||
elif line.startswith('@@'):
|
||||
sys.stdout.write(CYAN)
|
||||
|
||||
sys.stdout.write(line)
|
||||
sys.stdout.write(RESET)
|
||||
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Handle flag parsing."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Proxy and compare MPD protocol interactions.')
|
||||
parser.add_argument('--listen', default=':6600', type=parse_address,
|
||||
help='address:port to listen on.')
|
||||
parser.add_argument('--reference', default=':6601', type=parse_address,
|
||||
help='address:port for the reference backend.')
|
||||
parser.add_argument('--actual', default=':6602', type=parse_address,
|
||||
help='address:port for the actual backend.')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def parse_address(address):
|
||||
"""Convert host:port or port to address to pass to connect."""
|
||||
if ':' not in address:
|
||||
return ('', int(address))
|
||||
host, port = address.rsplit(':', 1)
|
||||
return (host, int(port))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parse_args()
|
||||
|
||||
def handle(client, address):
|
||||
"""Wrapper that adds reference and actual backends to proxy calls."""
|
||||
return proxy(client, address, args.reference, args.actual)
|
||||
|
||||
try:
|
||||
server.StreamServer(args.listen, handle).serve_forever()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
203
tools/idle.py
@ -1,203 +0,0 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
# This script is helper to systematicly test the behaviour of MPD's idle
|
||||
# command. It is simply provided as a quick hack, expect nothing more.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import pprint
|
||||
import socket
|
||||
|
||||
host = ''
|
||||
port = 6601
|
||||
|
||||
url = "13 - a-ha - White Canvas.mp3"
|
||||
artist = "a-ha"
|
||||
|
||||
data = {'id': None, 'id2': None, 'url': url, 'artist': artist}
|
||||
|
||||
# Commands to run before test requests to coerce MPD into right state
|
||||
setup_requests = [
|
||||
'clear',
|
||||
'add "%(url)s"',
|
||||
'add "%(url)s"',
|
||||
'add "%(url)s"',
|
||||
'play',
|
||||
#'pause', # Uncomment to test paused idle behaviour
|
||||
#'stop', # Uncomment to test stopped idle behaviour
|
||||
]
|
||||
|
||||
# List of commands to test for idle behaviour. Ordering of list is important in
|
||||
# order to keep MPD state as intended. Commands that are obviously
|
||||
# informational only or "harmfull" have been excluded.
|
||||
test_requests = [
|
||||
'add "%(url)s"',
|
||||
'addid "%(url)s" "1"',
|
||||
'clear',
|
||||
#'clearerror',
|
||||
#'close',
|
||||
#'commands',
|
||||
'consume "1"',
|
||||
'consume "0"',
|
||||
# 'count',
|
||||
'crossfade "1"',
|
||||
'crossfade "0"',
|
||||
#'currentsong',
|
||||
#'delete "1:2"',
|
||||
'delete "0"',
|
||||
'deleteid "%(id)s"',
|
||||
'disableoutput "0"',
|
||||
'enableoutput "0"',
|
||||
#'find',
|
||||
#'findadd "artist" "%(artist)s"',
|
||||
#'idle',
|
||||
#'kill',
|
||||
#'list',
|
||||
#'listall',
|
||||
#'listallinfo',
|
||||
#'listplaylist',
|
||||
#'listplaylistinfo',
|
||||
#'listplaylists',
|
||||
#'lsinfo',
|
||||
'move "0:1" "2"',
|
||||
'move "0" "1"',
|
||||
'moveid "%(id)s" "1"',
|
||||
'next',
|
||||
#'notcommands',
|
||||
#'outputs',
|
||||
#'password',
|
||||
'pause',
|
||||
#'ping',
|
||||
'play',
|
||||
'playid "%(id)s"',
|
||||
#'playlist',
|
||||
'playlistadd "foo" "%(url)s"',
|
||||
'playlistclear "foo"',
|
||||
'playlistadd "foo" "%(url)s"',
|
||||
'playlistdelete "foo" "0"',
|
||||
#'playlistfind',
|
||||
#'playlistid',
|
||||
#'playlistinfo',
|
||||
'playlistadd "foo" "%(url)s"',
|
||||
'playlistadd "foo" "%(url)s"',
|
||||
'playlistmove "foo" "0" "1"',
|
||||
#'playlistsearch',
|
||||
#'plchanges',
|
||||
#'plchangesposid',
|
||||
'previous',
|
||||
'random "1"',
|
||||
'random "0"',
|
||||
'rm "bar"',
|
||||
'rename "foo" "bar"',
|
||||
'repeat "0"',
|
||||
'rm "bar"',
|
||||
'save "bar"',
|
||||
'load "bar"',
|
||||
#'search',
|
||||
'seek "1" "10"',
|
||||
'seekid "%(id)s" "10"',
|
||||
#'setvol "10"',
|
||||
'shuffle',
|
||||
'shuffle "0:1"',
|
||||
'single "1"',
|
||||
'single "0"',
|
||||
#'stats',
|
||||
#'status',
|
||||
'stop',
|
||||
'swap "1" "2"',
|
||||
'swapid "%(id)s" "%(id2)s"',
|
||||
#'tagtypes',
|
||||
#'update',
|
||||
#'urlhandlers',
|
||||
#'volume',
|
||||
]
|
||||
|
||||
|
||||
def create_socketfile():
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect((host, port))
|
||||
sock.settimeout(0.5)
|
||||
fd = sock.makefile('rw', 1) # 1 = line buffered
|
||||
fd.readline() # Read banner
|
||||
return fd
|
||||
|
||||
|
||||
def wait(fd, prefix=None, collect=None):
|
||||
while True:
|
||||
line = fd.readline().rstrip()
|
||||
if prefix:
|
||||
logging.debug('%s: %s', prefix, repr(line))
|
||||
if line.split()[0] in ('OK', 'ACK'):
|
||||
break
|
||||
|
||||
|
||||
def collect_ids(fd):
|
||||
fd.write('playlistinfo\n')
|
||||
|
||||
ids = []
|
||||
while True:
|
||||
line = fd.readline()
|
||||
if line.split()[0] == 'OK':
|
||||
break
|
||||
if line.split()[0] == 'Id:':
|
||||
ids.append(line.split()[1])
|
||||
return ids
|
||||
|
||||
|
||||
def main():
|
||||
subsystems = {}
|
||||
|
||||
command = create_socketfile()
|
||||
|
||||
for test in test_requests:
|
||||
# Remove any old ids
|
||||
del data['id']
|
||||
del data['id2']
|
||||
|
||||
# Run setup code to force MPD into known state
|
||||
for setup in setup_requests:
|
||||
command.write(setup % data + '\n')
|
||||
wait(command)
|
||||
|
||||
data['id'], data['id2'] = collect_ids(command)[:2]
|
||||
|
||||
# This connection needs to be make after setup commands are done or
|
||||
# else they will cause idle events.
|
||||
idle = create_socketfile()
|
||||
|
||||
# Wait for new idle events
|
||||
idle.write('idle\n')
|
||||
|
||||
test = test % data
|
||||
|
||||
logging.debug('idle: %s', repr('idle'))
|
||||
logging.debug('command: %s', repr(test))
|
||||
|
||||
command.write(test + '\n')
|
||||
wait(command, prefix='command')
|
||||
|
||||
while True:
|
||||
try:
|
||||
line = idle.readline().rstrip()
|
||||
except socket.timeout:
|
||||
# Abort try if we time out.
|
||||
idle.write('noidle\n')
|
||||
break
|
||||
|
||||
logging.debug('idle: %s', repr(line))
|
||||
|
||||
if line == 'OK':
|
||||
break
|
||||
|
||||
request_type = test.split()[0]
|
||||
subsystem = line.split()[1]
|
||||
subsystems.setdefault(request_type, set()).add(subsystem)
|
||||
|
||||
logging.debug('---')
|
||||
|
||||
pprint.pprint(subsystems)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||