diff --git a/docs/api/index.rst b/docs/api/index.rst index f58552b7..bede978b 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -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 diff --git a/docs/api/zeroconf.rst b/docs/api/zeroconf.rst new file mode 100644 index 00000000..7cdd93f0 --- /dev/null +++ b/docs/api/zeroconf.rst @@ -0,0 +1,11 @@ +.. _zeroconf-api: + +************ +Zeroconf API +************ + +.. module:: mopidy.zeroconf + :synopsis: Helper for publishing of services on Zeroconf + +.. autoclass:: Zeroconf + :members: diff --git a/docs/_static/dz0ny-mopidy-lux.png b/docs/clients/dz0ny-mopidy-lux.png similarity index 100% rename from docs/_static/dz0ny-mopidy-lux.png rename to docs/clients/dz0ny-mopidy-lux.png diff --git a/docs/clients/http.rst b/docs/clients/http.rst index a31636cd..9ef3b131 100644 --- a/docs/clients/http.rst +++ b/docs/clients/http.rst @@ -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 diff --git a/docs/_static/martijnboland-moped.png b/docs/clients/martijnboland-moped.png similarity index 100% rename from docs/_static/martijnboland-moped.png rename to docs/clients/martijnboland-moped.png diff --git a/docs/_static/mpd-client-gmpc.png b/docs/clients/mpd-client-gmpc.png similarity index 100% rename from docs/_static/mpd-client-gmpc.png rename to docs/clients/mpd-client-gmpc.png diff --git a/docs/_static/mpd-client-mpad.jpg b/docs/clients/mpd-client-mpad.jpg similarity index 100% rename from docs/_static/mpd-client-mpad.jpg rename to docs/clients/mpd-client-mpad.jpg diff --git a/docs/_static/mpd-client-mpdroid.jpg b/docs/clients/mpd-client-mpdroid.jpg similarity index 100% rename from docs/_static/mpd-client-mpdroid.jpg rename to docs/clients/mpd-client-mpdroid.jpg diff --git a/docs/_static/mpd-client-mpod.jpg b/docs/clients/mpd-client-mpod.jpg similarity index 100% rename from docs/_static/mpd-client-mpod.jpg rename to docs/clients/mpd-client-mpod.jpg diff --git a/docs/_static/mpd-client-ncmpcpp.png b/docs/clients/mpd-client-ncmpcpp.png similarity index 100% rename from docs/_static/mpd-client-ncmpcpp.png rename to docs/clients/mpd-client-ncmpcpp.png diff --git a/docs/_static/mpd-client-sonata.png b/docs/clients/mpd-client-sonata.png similarity index 100% rename from docs/_static/mpd-client-sonata.png rename to docs/clients/mpd-client-sonata.png diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index 0993303d..4a2736fe 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -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 `_ 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 `_ 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 diff --git a/docs/clients/mpris.rst b/docs/clients/mpris.rst index e1bd4bff..650372e6 100644 --- a/docs/clients/mpris.rst +++ b/docs/clients/mpris.rst @@ -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 diff --git a/docs/_static/rompr.png b/docs/clients/rompr.png similarity index 100% rename from docs/_static/rompr.png rename to docs/clients/rompr.png diff --git a/docs/_static/ubuntu-sound-menu.png b/docs/clients/ubuntu-sound-menu.png similarity index 100% rename from docs/_static/ubuntu-sound-menu.png rename to docs/clients/ubuntu-sound-menu.png diff --git a/docs/_static/woutervanwijk-mopidy-webclient.png b/docs/clients/woutervanwijk-mopidy-webclient.png similarity index 100% rename from docs/_static/woutervanwijk-mopidy-webclient.png rename to docs/clients/woutervanwijk-mopidy-webclient.png diff --git a/docs/conf.py b/docs/conf.py index 25e0b145..5417a55c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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_') diff --git a/docs/devtools.rst b/docs/devtools.rst index 858cc7f8..64bb7e6b 100644 --- a/docs/devtools.rst +++ b/docs/devtools.rst @@ -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 ===================== diff --git a/docs/_static/raspberry-pi-by-jwrodgers.jpg b/docs/installation/raspberry-pi-by-jwrodgers.jpg similarity index 100% rename from docs/_static/raspberry-pi-by-jwrodgers.jpg rename to docs/installation/raspberry-pi-by-jwrodgers.jpg diff --git a/docs/installation/raspberrypi.rst b/docs/installation/raspberrypi.rst index e266dee2..4bc17a26 100644 --- a/docs/installation/raspberrypi.rst +++ b/docs/installation/raspberrypi.rst @@ -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 diff --git a/mopidy/__main__.py b/mopidy/__main__.py index f66ac6c1..2d5ae819 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -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: diff --git a/mopidy/commands.py b/mopidy/commands.py index 1bba63fa..f81174d5 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -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 diff --git a/mopidy/config/convert.py b/mopidy/config/convert.py index 87bf4ed5..7012b56e 100644 --- a/mopidy/config/convert.py +++ b/mopidy/config/convert.py @@ -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.') diff --git a/mopidy/frontends/http/actor.py b/mopidy/frontends/http/actor.py index 4e3493d4..5aef3506 100644 --- a/mopidy/frontends/http/actor.py +++ b/mopidy/frontends/http/actor.py @@ -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 diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index 9df7ba07..fb063f6c 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -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') diff --git a/mopidy/models.py b/mopidy/models.py index 04d71591..0e40a8f6 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -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 diff --git a/mopidy/utils/zeroconf.py b/mopidy/zeroconf.py similarity index 76% rename from mopidy/utils/zeroconf.py rename to mopidy/zeroconf.py index acd25ef1..671bebc7 100644 --- a/mopidy/utils/zeroconf.py +++ b/mopidy/zeroconf.py @@ -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() diff --git a/tests/models_test.py b/tests/models_test.py index 9f43e624..50faf89e 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -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' diff --git a/tools/debug-proxy.py b/tools/debug-proxy.py deleted file mode 100755 index 938afa57..00000000 --- a/tools/debug-proxy.py +++ /dev/null @@ -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 diff --git a/tools/idle.py b/tools/idle.py deleted file mode 100644 index 122e998d..00000000 --- a/tools/idle.py +++ /dev/null @@ -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()