Browse code
Initial console and web clients
- mpdprotocol updated so it supports idle mode
- various bugs in responsedict fixed: mostly concerned with improper
results from as_table()
- templates/playlist.html and web_bridge.py provide a twisted web
resource that displays the playlist.
- urwid_playlist.py implements a command-line curses playlist
Showing 5 changed files
... | ... |
@@ -1,53 +1,103 @@ |
1 | 1 |
from __future__ import print_function |
2 |
-import collections |
|
2 |
+import itertools |
|
3 | 3 |
|
4 | 4 |
from twisted.internet import defer |
5 | 5 |
from twisted.internet import protocol |
6 | 6 |
from twisted.internet import reactor |
7 |
+from twisted.internet import task |
|
7 | 8 |
from twisted.protocols import basic |
8 | 9 |
|
9 | 10 |
from responsedict import ResponseDict |
10 | 11 |
|
12 |
+import os |
|
13 |
+import sys |
|
14 |
+def get_client(host='', port=6600): |
|
15 |
+ '''returns a deferred''' |
|
16 |
+ if host is '': |
|
17 |
+ host = os.getenv('MPD_HOST', 'localhost') |
|
18 |
+ #print("Using default config: %(host)s:%(port)s" % dict(host=host, port=port), file=sys.stderr) |
|
19 |
+ ccreator = protocol.ClientCreator(reactor, MPDProtocol) |
|
20 |
+ return ccreator.connectTCP(host, port) |
|
21 |
+ |
|
22 |
+ |
|
11 | 23 |
class MPDProtocol(basic.LineReceiver): |
12 | 24 |
delimiter = '\x0a' |
13 | 25 |
|
14 | 26 |
def __init__(self, *a, **kw): |
15 | 27 |
#basic.LineReceiver.__init__(self, *a, **kw) |
16 | 28 |
self.commands = [] |
29 |
+ self.notifications = [] |
|
17 | 30 |
self.serverinfo=None |
31 |
+ #self.lineReceived = self.noidlereceive |
|
32 |
+ self.listeners = {} |
|
33 |
+ self._idle = False |
|
18 | 34 |
|
19 | 35 |
def fallback(self, data): |
20 | 36 |
print(data.as_list()) |
21 | 37 |
|
22 |
- def lineReceived(self, line): |
|
38 |
+ def check_servername(self, line): |
|
23 | 39 |
if self.serverinfo is None and line.startswith('OK'): |
24 | 40 |
self.serverinfo = line.split() |
41 |
+ return True |
|
42 |
+ set_servname = check_servername |
|
43 |
+ |
|
44 |
+ def lineReceived(self, line): |
|
45 |
+ if self.check_servername(line): pass |
|
25 | 46 |
else: |
47 |
+ #print('line received:', repr(line)) |
|
26 | 48 |
if line.strip() == 'OK': |
49 |
+ self.reset_idle() |
|
27 | 50 |
method, deferred, data = self.commands.pop(0) |
28 | 51 |
d = reactor.callLater(0, deferred.callback, (method, data)) |
52 |
+ for listener in self.listeners.get(method,[]): |
|
53 |
+ listener.notify(self, line) |
|
29 | 54 |
else: |
30 | 55 |
self.commands[0][2].add_line(line.rstrip()) |
31 | 56 |
|
32 |
- def sendCommand(self, command): |
|
33 |
- self.commands.append((command, defer.Deferred(), ResponseDict())) |
|
57 |
+ def add_listener(self, listener, command): |
|
58 |
+ self.listeners.setdefault(command, []).append(listener) |
|
59 |
+ return listener |
|
60 |
+ |
|
61 |
+ def reset_idle(self): self._idle = False |
|
62 |
+ def idle(self, subsystems=None, listener=None): |
|
63 |
+ if subsystems is None:subsystems = [] |
|
64 |
+ result = self.sendCommand('idle', *subsystems) |
|
65 |
+ self._idle = True |
|
66 |
+ if listener is not None: |
|
67 |
+ self.add_listener(listener, 'idle') |
|
68 |
+ return result |
|
69 |
+ |
|
70 |
+ def sendCommand(self, command, *args): |
|
71 |
+ result = None |
|
72 |
+ if self._idle and command not in {'idle', 'noidle'}: |
|
73 |
+ if self.commands[0][0] == 'idle': self.commands.pop(0) |
|
74 |
+ self.sendCommand('noidle') |
|
75 |
+ if args: |
|
76 |
+ command = ' '.join(itertools.chain([command], args)) |
|
34 | 77 |
self.transport.write('%s\n' % command) |
35 |
- return self.commands[-1][1] |
|
78 |
+ self.commands.append((command, defer.Deferred(), ResponseDict())) |
|
79 |
+ result = self.commands[-1][1] |
|
80 |
+ return result |
|
36 | 81 |
|
37 |
-def handle(result, mpdClient): |
|
38 |
- method,data = result |
|
39 |
- print(data.as_list()) |
|
40 |
- return reactor.callLater(0, run, mpdClient) |
|
82 |
+import pprint |
|
83 |
+import traceback, inspect |
|
41 | 84 |
|
85 |
+@defer.inlineCallbacks |
|
42 | 86 |
def run(mpdClient): |
43 |
- mpdClient.sendCommand(raw_input('command? ')).addCallback(handle, mpdClient), |
|
87 |
+ #NOTE: remember, idleHandler not used! |
|
88 |
+ method, result = yield mpdClient.idle(['playlist']) |
|
89 |
+ print(method, result) |
|
90 |
+ method, result = yield mpdClient.sendCommand('playlistinfo') |
|
91 |
+ print(result.as_list()) |
|
92 |
+ import time |
|
93 |
+ time.sleep(1) |
|
94 |
+ run(mpdClient) |
|
44 | 95 |
|
45 | 96 |
def fail(*a, **kw): |
46 | 97 |
print(*a, **kw) |
47 | 98 |
|
48 | 99 |
if __name__ == '__main__': |
49 |
- ccreator = protocol.ClientCreator(reactor, MPDProtocol) |
|
50 |
- ccreator.connectTCP('localhost', 6600).addCallback(run).addErrback(fail) |
|
100 |
+ get_client().addCallbacks(run,fail) |
|
51 | 101 |
reactor.run() |
52 | 102 |
|
53 | 103 |
|
... | ... |
@@ -19,9 +19,10 @@ class ResponseDict(collections.Mapping): |
19 | 19 |
def add_line(self, line): |
20 | 20 |
k,_,v = line.strip().partition(':') |
21 | 21 |
|
22 |
- if k in self.data[-1]: self.data.append({}) |
|
23 |
- self.data[-1][k] = v.strip() |
|
24 |
- return (k,self.data[-1][k]) |
|
22 |
+ if k in self.data[-1] and k.lower() == 'file': |
|
23 |
+ self.data.append({}) |
|
24 |
+ self.data[-1].setdefault(k, []).append(v.strip()) |
|
25 |
+ return (k,self.data[-1][k][-1]) |
|
25 | 26 |
|
26 | 27 |
def __getitem__(self,it): |
27 | 28 |
return self.data[-1][it] |
... | ... |
@@ -33,7 +34,12 @@ class ResponseDict(collections.Mapping): |
33 | 34 |
return len(self.data[-1]) |
34 | 35 |
|
35 | 36 |
def as_list(self): |
36 |
- return self.data |
|
37 |
+ out = [] |
|
38 |
+ for datum in self.data: |
|
39 |
+ out.append({}) |
|
40 |
+ for k in datum: |
|
41 |
+ out[-1].setdefault(k,[]).extend(item for item in datum[k]) |
|
42 |
+ return out |
|
37 | 43 |
|
38 | 44 |
def as_table(self): |
39 | 45 |
keys = set() |
... | ... |
@@ -42,7 +48,7 @@ class ResponseDict(collections.Mapping): |
42 | 48 |
keys.update(dct) |
43 | 49 |
for dct in self.data: |
44 | 50 |
for key in keys: |
45 |
- out.setdefault(key, []).append(dct.get(key,'')) |
|
51 |
+ out.setdefault(key, []).append(dct.get(key,[''])) |
|
46 | 52 |
return out |
47 | 53 |
|
48 | 54 |
|
49 | 55 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,43 @@ |
1 |
+<html> |
|
2 |
+<head> |
|
3 |
+ <title>{{ title }}</title> |
|
4 |
+ <STYLE type="text/css"> |
|
5 |
+ .album { |
|
6 |
+ max-width: 15em; |
|
7 |
+ } |
|
8 |
+ .artist { |
|
9 |
+ max-width: 20em; |
|
10 |
+ } |
|
11 |
+ .title { |
|
12 |
+ max-width: 25em; |
|
13 |
+ } |
|
14 |
+ .genre { |
|
15 |
+ max-width: 5em; |
|
16 |
+ } |
|
17 |
+ |
|
18 |
+ li > span { |
|
19 |
+ display: table-cell; |
|
20 |
+ vertical-align: middle; |
|
21 |
+ } |
|
22 |
+ tr:nth-child(even){ |
|
23 |
+ background: hsl(120,30%,90%); |
|
24 |
+ } |
|
25 |
+ table { |
|
26 |
+ border-collapse: collapse; |
|
27 |
+ } |
|
28 |
+ tr + tr { |
|
29 |
+ border-top: solid thin grey; |
|
30 |
+ |
|
31 |
+ } |
|
32 |
+ </STYLE> |
|
33 |
+</head> |
|
34 |
+<body> |
|
35 |
+ <table class="playlist"> |
|
36 |
+ {% for item in playlist %} |
|
37 |
+ <tr>{% for (class,texts) in item %} |
|
38 |
+ {% for text in texts %}<td class="{{class}}">{{text.decode('utf-8')}}</td>{%endfor%} |
|
39 |
+ {%endfor%}</tr> |
|
40 |
+ {% endfor %} |
|
41 |
+ </table> |
|
42 |
+</body> |
|
43 |
+</html> |
0 | 44 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,67 @@ |
1 |
+import mpdprotocol |
|
2 |
+import responsedict |
|
3 |
+import urwid |
|
4 |
+import itertools |
|
5 |
+from twisted.internet import defer |
|
6 |
+ |
|
7 |
+class SelectableText(urwid.Text): |
|
8 |
+ def keypress(self, __, key): return key |
|
9 |
+ def selectable(self): return True |
|
10 |
+ |
|
11 |
+class Playlist(urwid.WidgetWrap): |
|
12 |
+ def __init__(self, columns, plylst): |
|
13 |
+ self.playlist = plylst |
|
14 |
+ self.columns = columns |
|
15 |
+ self.texts = self.get_texts(self.playlist.as_table(), cols) |
|
16 |
+ |
|
17 |
+ self.titles = urwid.Columns([('weight',w,SelectableText(c)) for w,c,_ in cols],1) |
|
18 |
+ self.header = urwid.Pile([self.titles, urwid.Divider('-')]) |
|
19 |
+ widget = self.make_playlist() |
|
20 |
+ widget = urwid.Frame(widget, self.header) |
|
21 |
+ urwid.WidgetWrap.__init__(self, widget) |
|
22 |
+ |
|
23 |
+ def get_texts(self, choices, columns): |
|
24 |
+ texts = [] |
|
25 |
+ for weight,col,transform in columns: |
|
26 |
+ texts.append([(weight,transform(x[0])) for x in choices.get(col)]) |
|
27 |
+ return texts |
|
28 |
+ |
|
29 |
+ def make_playlist(self): |
|
30 |
+ body = [] |
|
31 |
+ for x in itertools.izip_longest(*self.texts): |
|
32 |
+ new_widg = [('weight',wgt,SelectableText(it,wrap='clip')) for wgt,it in x] |
|
33 |
+ new_widg = urwid.Columns(new_widg, 1) |
|
34 |
+ new_widg = urwid.AttrMap(new_widg, 'unselected', 'selected') |
|
35 |
+ body.append(new_widg) |
|
36 |
+ list_walker = urwid.SimpleFocusListWalker(body) |
|
37 |
+ return urwid.ListBox(urwid.SimpleFocusListWalker(body[:])) |
|
38 |
+ |
|
39 |
+def exit_on_q(key): |
|
40 |
+ if key in ('q', 'Q'): |
|
41 |
+ try: raise urwid.ExitMainLoop() |
|
42 |
+ finally: reactor.stop() |
|
43 |
+ |
|
44 |
+cols = [ |
|
45 |
+ (1,'Pos', lambda x: str(int(x)+1)), |
|
46 |
+ (4,'Album', lambda x:x), |
|
47 |
+ (3,'Artist', lambda x:x), |
|
48 |
+ (5,'Title', lambda x:x) |
|
49 |
+] |
|
50 |
+ |
|
51 |
+@defer.inlineCallbacks |
|
52 |
+def get_playlist(): |
|
53 |
+ client = yield mpdprotocol.get_client() |
|
54 |
+ res = yield client.sendCommand('playlistinfo') |
|
55 |
+ method, data = res |
|
56 |
+ loop.widget = Playlist(cols, data) |
|
57 |
+ loop.draw_screen() |
|
58 |
+ |
|
59 |
+from twisted.internet import reactor |
|
60 |
+palette = [ |
|
61 |
+ ('selected', 'white', 'black'), |
|
62 |
+ ('selected', 'black', 'white'), |
|
63 |
+] |
|
64 |
+loop = urwid.MainLoop(urwid.SolidFill(), palette, event_loop=urwid.TwistedEventLoop(), unhandled_input=exit_on_q) |
|
65 |
+reactor.callWhenRunning(get_playlist) |
|
66 |
+loop.run() |
|
67 |
+ |
0 | 68 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,65 @@ |
1 |
+# -*- coding: utf-8 -*- |
|
2 |
+from twisted.web import server, resource |
|
3 |
+from twisted.internet import reactor |
|
4 |
+from twisted.internet import task |
|
5 |
+ |
|
6 |
+import mpdprotocol |
|
7 |
+import jinja2 |
|
8 |
+import json |
|
9 |
+ |
|
10 |
+class MPDResource(resource.Resource): |
|
11 |
+ isLeaf = True |
|
12 |
+ jinja2env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates')) |
|
13 |
+ |
|
14 |
+ def do(self, mpc, request): |
|
15 |
+ d = mpc.sendCommand('playlistinfo') |
|
16 |
+ |
|
17 |
+ cb = self.render_playlist |
|
18 |
+ if 'json' in request.args: |
|
19 |
+ if 'table' in request.args: |
|
20 |
+ cb = self.render_as_table_json |
|
21 |
+ else: |
|
22 |
+ cb = self.render_as_json |
|
23 |
+ d.addCallback(cb, request) |
|
24 |
+ return d |
|
25 |
+ |
|
26 |
+ def render_as_table_json(self, result, request): |
|
27 |
+ method, data = result |
|
28 |
+ request.setHeader('Content-Type', 'application/json') |
|
29 |
+ request.write(json.dumps(data.as_table())); |
|
30 |
+ request.finish() |
|
31 |
+ |
|
32 |
+ def render_as_json(self, result, request): |
|
33 |
+ method, data = result |
|
34 |
+ request.setHeader('Content-Type', 'application/json') |
|
35 |
+ request.write(json.dumps(data.as_list())); |
|
36 |
+ request.finish() |
|
37 |
+ |
|
38 |
+ def render_playlist(self, result, request): |
|
39 |
+ print('rendering playlist!') |
|
40 |
+ method, data = result |
|
41 |
+ template = self.jinja2env.get_template('playlist.html') |
|
42 |
+ tdata = [] |
|
43 |
+ for datum in data.as_list(): |
|
44 |
+ tdata.append([]) |
|
45 |
+ tdata[-1].append(('pos', datum['Pos'])) |
|
46 |
+ tdata[-1].append(('title', datum['Title'])) |
|
47 |
+ tdata[-1].append(('artist', datum['Artist'])) |
|
48 |
+ tdata[-1].append(('album', datum['Album'])) |
|
49 |
+ tdata[-1].append(('genre', datum['Genre'])) |
|
50 |
+ request.write(template.render(playlist=tdata).encode('utf-8')) |
|
51 |
+ request.finish() |
|
52 |
+ |
|
53 |
+ def failed(self, failure, request): |
|
54 |
+ print "whjat should I do here?" |
|
55 |
+ print self, failure, request |
|
56 |
+ failure.printTraceback() |
|
57 |
+ failure.raiseException() |
|
58 |
+ |
|
59 |
+ def render_GET(self, request): |
|
60 |
+ print('ello world!') |
|
61 |
+ d = mpdprotocol.get_client().addCallback(self.do, request) |
|
62 |
+ d.addErrback(self.failed, request) |
|
63 |
+ return server.NOT_DONE_YET |
|
64 |
+ |
|
65 |
+resource = MPDResource() |