git.fiddlerwoaroof.com
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

fiddlerwoaroof authored on 05/08/2014 04:33:17
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()