Commit | Line | Data |
---|---|---|
c0897f29 FT |
1 | #!/usr/bin/python3 |
2 | ||
3 | import os, sys, getopt, io, termios | |
4 | import socket, json, pprint | |
5 | ||
6 | class key(object): | |
7 | def __init__(self, nm): | |
8 | self.nm = nm | |
9 | def __repr__(self): | |
10 | return "<key %s>" % (self.nm,) | |
11 | class akey(key): | |
12 | def __init__(self, cc): | |
13 | super().__init__(repr(cc)) | |
14 | self.cc = cc | |
15 | def __eq__(self, o): | |
16 | return self.cc == o | |
17 | ||
18 | class rawtty(object): | |
19 | K_LEFT = key("K_LEFT") | |
20 | K_RIGHT = key("K_RIGHT") | |
21 | K_UP = key("K_UP") | |
22 | K_DOWN = key("K_DOWN") | |
23 | MOD_SHIFT = key("MOD_SHIFT") | |
24 | MOD_META = key("MOD_META") | |
25 | MOD_CTRL = key("MOD_CTRL") | |
26 | ||
27 | def __init__(self, *, path="/dev/tty"): | |
28 | self.io = io.FileIO(os.open(path, os.O_RDWR | os.O_NOCTTY), "r+") | |
29 | attr = termios.tcgetattr(self.io.fileno()) | |
30 | self.bka = list(attr) | |
31 | attr[3] &= ~termios.ECHO & ~termios.ICANON | |
32 | termios.tcsetattr(self.io.fileno(), termios.TCSANOW, attr) | |
33 | ||
34 | def getc(self): | |
35 | b = self.io.read(1) | |
36 | return None if b == b"" else b[0] | |
37 | ||
38 | _csikeys = {'A': K_UP, 'B': K_DOWN, 'C': K_RIGHT, 'D': K_LEFT} | |
39 | def readkey(self): | |
40 | c = self.getc() | |
41 | if c == 27: | |
42 | c = self.getc() | |
43 | if c == 27: | |
44 | return akey("\x1b"), set() | |
45 | elif c == ord('O'): | |
46 | return None, set() | |
47 | elif c == ord('['): | |
48 | pars = [] | |
49 | par = None | |
50 | while True: | |
51 | c = self.getc() | |
52 | if 48 <= c <= 57: | |
53 | if par is None: | |
54 | par = 0 | |
55 | par = (par * 10) + (c - 48) | |
56 | elif c == ord(';'): | |
57 | pars.append(par) | |
58 | par = None | |
59 | else: | |
60 | if par is not None: | |
61 | pars.append(par) | |
62 | break | |
63 | if c == ord('~'): | |
64 | key = None | |
65 | elif chr(c) in self._csikeys: | |
66 | key = self._csikeys[chr(c)] | |
67 | else: | |
68 | key = None | |
69 | mods = set() | |
70 | if len(pars) > 1: | |
71 | if (pars[1] - 1) & 1: | |
72 | mods.add(self.MOD_SHIFT) | |
73 | if (pars[1] - 1) & 2: | |
74 | mods.add(self.MOD_META) | |
75 | if (pars[1] - 1) & 4: | |
76 | mods.add(self.MOD_CTRL) | |
77 | return key, mods | |
78 | else: | |
79 | return akey(chr(c)), [self.MOD_META] | |
80 | else: | |
81 | return akey(chr(c)), set() | |
82 | ||
83 | def close(self): | |
84 | termios.tcsetattr(self.io.fileno(), termios.TCSANOW, self.bka) | |
85 | self.io.close() | |
86 | ||
87 | def __enter__(self): | |
88 | return self | |
89 | ||
90 | def __exit__(self, *exc): | |
91 | self.close() | |
92 | return False | |
93 | ||
94 | class target(object): | |
95 | def __init__(self, path): | |
96 | self.path = path | |
97 | self.sk = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | |
98 | self.sk.connect(path) | |
99 | self.obuf = bytearray() | |
100 | self.ibuf = bytearray() | |
101 | ||
102 | def write(self, data): | |
103 | self.obuf.extend(data) | |
104 | ||
105 | def flush(self): | |
106 | while len(self.obuf) > 0: | |
107 | ret = self.sk.send(self.obuf) | |
108 | self.obuf[:ret] = b"" | |
109 | ||
110 | def recv(self, hint=1024): | |
111 | data = self.sk.recv(hint) | |
112 | if data == b"": | |
113 | raise EOFError() | |
114 | self.ibuf.extend(data) | |
115 | ||
116 | def readline(self): | |
117 | p = 0 | |
118 | while True: | |
119 | p2 = self.ibuf.find(b'\n', p) | |
120 | if p2 != -1: | |
121 | ret = bytes(self.ibuf[:p2]) | |
122 | self.ibuf[:p2 + 1] = b"" | |
123 | return ret | |
124 | p = len(self.ibuf) | |
125 | self.recv() | |
126 | ||
127 | def send(self, data): | |
128 | self.write(data) | |
129 | self.flush() | |
130 | ||
131 | def getresp(self): | |
132 | while True: | |
133 | resp = json.loads(self.readline().decode("utf-8")) | |
134 | if "event" in resp: | |
135 | continue | |
136 | return resp | |
137 | ||
138 | def runcmd(self, cmd): | |
139 | self.send(json.dumps(cmd).encode("utf-8") + b"\n") | |
140 | resp = self.getresp() | |
141 | if "error" not in resp: | |
142 | sys.stderr.write("mpsync: strange response from %s: %r\n" % (self.path, resp)) | |
143 | if resp["error"] != "success": | |
144 | sys.stderr.write("mpsync: error response from %s: %r\n" % (self.path, resp)) | |
145 | return resp | |
146 | ||
147 | def getprop(self, pname): | |
148 | return self.runcmd({"command": ["get_property", pname]})["data"] | |
149 | ||
150 | def setprop(self, pname, val): | |
151 | self.runcmd({"command": ["set_property", pname, val]}) | |
152 | ||
153 | def usage(out): | |
154 | out.write("usage: mpsync [-h] SOCKET...\n") | |
155 | out.write("players: mpv -input-unix-socket SOCKET -pause [ARGS...] FILE\n") | |
156 | ||
157 | opts, args = getopt.getopt(sys.argv[1:], "h") | |
158 | for o, a in opts: | |
159 | if o == "-h": | |
160 | usage(sys.stdout) | |
161 | sys.exit(0) | |
162 | if len(args) < 1: | |
163 | usage(sys.stderr) | |
164 | sys.exit(1) | |
165 | for path in args: | |
166 | if not os.path.exists(path): | |
167 | sys.stderr.write("mpsync: %s: no such file or directory\n" % path) | |
168 | sys.exit(1) | |
169 | ||
170 | targets = [] | |
171 | for path in args: | |
172 | targets.append(target(path)) | |
173 | ||
174 | def runcmd(*cmd): | |
175 | cmd = {"command": cmd} | |
176 | for tgt in targets: | |
177 | tgt.runcmd(cmd) | |
178 | ||
179 | def simulcmd(*cmd): | |
180 | cmd = json.dumps({"command": cmd}).encode("utf-8") + b"\n" | |
181 | for tgt in targets: | |
182 | tgt.send(cmd) | |
183 | for tgt in targets: | |
184 | resp = tgt.getresp() | |
185 | if "error" not in resp: | |
186 | sys.stderr.write("mpsync: strange response from %s: %r\n" % (tgt.path, resp)) | |
187 | if resp["error"] != "success": | |
188 | sys.stderr.write("mpsync: error response from %s: %r\n" % (tgt.path, resp)) | |
189 | ||
190 | def relseek(ss, offsets): | |
191 | cur = targets[0].getprop("time-pos") | |
192 | for tgt, off in zip(targets, offsets): | |
193 | tgt.setprop("time-pos", cur + ss + off) | |
194 | ||
195 | def getoffsets(): | |
196 | opos = targets[0].getprop("time-pos") | |
197 | ret = [] | |
198 | for tgt in targets: | |
199 | ret.append(tgt.getprop("time-pos") - opos) | |
200 | return ret | |
201 | ||
202 | def main(tty): | |
203 | paused = targets[0].getprop("pause") | |
204 | runcmd("set_property", "hr-seek", "yes") | |
205 | offsets = [0.0] * len(targets) | |
206 | while True: | |
207 | c, mods = tty.readkey() | |
208 | if c == 'q': | |
209 | return | |
210 | elif c == 'Q': | |
211 | runcmd("quit") | |
212 | return | |
213 | elif c == ' ': | |
214 | paused = not paused | |
215 | simulcmd("set_property", "pause", paused) | |
216 | elif c == rawtty.K_LEFT and not mods: | |
217 | relseek(-10, offsets) | |
218 | elif c == rawtty.K_RIGHT and not mods: | |
219 | relseek(10, offsets) | |
220 | elif c == rawtty.K_UP and not mods: | |
221 | relseek(60, offsets) | |
222 | elif c == rawtty.K_DOWN and not mods: | |
223 | relseek(-60, offsets) | |
224 | elif c == rawtty.K_LEFT and mods == {rawtty.MOD_SHIFT}: | |
225 | relseek(-2, offsets) | |
226 | elif c == rawtty.K_RIGHT and mods == {rawtty.MOD_SHIFT}: | |
227 | relseek(2, offsets) | |
228 | elif c == rawtty.K_UP and mods == {rawtty.MOD_SHIFT}: | |
229 | relseek(5, offsets) | |
230 | elif c == rawtty.K_DOWN and mods == {rawtty.MOD_SHIFT}: | |
231 | relseek(-5, offsets) | |
232 | elif c == 'S': | |
233 | offsets = getoffsets() | |
234 | print(offsets) | |
235 | elif c == '.': | |
236 | runcmd("frame_step") | |
237 | paused = True | |
238 | elif c == ',': | |
239 | runcmd("frame_back_step") | |
240 | paused = True | |
241 | ||
242 | with rawtty() as tty: | |
243 | try: | |
244 | main(tty) | |
245 | except KeyboardInterrupt: | |
246 | pass |