Added logtail.
[utils.git] / mpsync
CommitLineData
c0897f29
FT
1#!/usr/bin/python3
2
3import os, sys, getopt, io, termios
4import socket, json, pprint
5
6class key(object):
7 def __init__(self, nm):
8 self.nm = nm
9 def __repr__(self):
10 return "<key %s>" % (self.nm,)
11class 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
18class 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
94class 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
28b58da2
FT
138 def runcmd(self, *cmd):
139 self.send(json.dumps({"command": cmd}).encode("utf-8") + b"\n")
c0897f29
FT
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):
28b58da2 148 return self.runcmd("get_property", pname)["data"]
c0897f29
FT
149
150 def setprop(self, pname, val):
28b58da2 151 self.runcmd("set_property", pname, val)
c0897f29
FT
152
153def usage(out):
154 out.write("usage: mpsync [-h] SOCKET...\n")
155 out.write("players: mpv -input-unix-socket SOCKET -pause [ARGS...] FILE\n")
156
157opts, args = getopt.getopt(sys.argv[1:], "h")
158for o, a in opts:
159 if o == "-h":
160 usage(sys.stdout)
161 sys.exit(0)
162if len(args) < 1:
163 usage(sys.stderr)
164 sys.exit(1)
165for 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
170targets = []
171for path in args:
172 targets.append(target(path))
173
174def runcmd(*cmd):
c0897f29 175 for tgt in targets:
28b58da2 176 tgt.runcmd(*cmd)
c0897f29
FT
177
178def simulcmd(*cmd):
179 cmd = json.dumps({"command": cmd}).encode("utf-8") + b"\n"
180 for tgt in targets:
181 tgt.send(cmd)
182 for tgt in targets:
183 resp = tgt.getresp()
184 if "error" not in resp:
185 sys.stderr.write("mpsync: strange response from %s: %r\n" % (tgt.path, resp))
186 if resp["error"] != "success":
187 sys.stderr.write("mpsync: error response from %s: %r\n" % (tgt.path, resp))
188
189def relseek(ss, offsets):
190 cur = targets[0].getprop("time-pos")
191 for tgt, off in zip(targets, offsets):
192 tgt.setprop("time-pos", cur + ss + off)
193
194def getoffsets():
195 opos = targets[0].getprop("time-pos")
196 ret = []
197 for tgt in targets:
198 ret.append(tgt.getprop("time-pos") - opos)
199 return ret
200
201def main(tty):
c0897f29 202 runcmd("set_property", "hr-seek", "yes")
28b58da2
FT
203 paused = targets[0].getprop("pause")
204 mutemode = 0
c0897f29
FT
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)
28b58da2
FT
232 elif c == 's':
233 relseek(0, offsets)
234 elif c == 'm':
235 mutemode = (mutemode + 1) % 3
236 if mutemode == 0:
237 for tgt in targets:
238 tgt.setprop("mute", False)
239 elif mutemode == 1:
240 for i, tgt in enumerate(targets):
241 tgt.setprop("mute", i != 0)
242 elif mutemode == 2:
243 for tgt in targets:
244 tgt.setprop("mute", True)
245 targets[0].runcmd("show_text", "Audio mode: %s" % ["All", "One", "None"][mutemode])
c0897f29
FT
246 elif c == 'S':
247 offsets = getoffsets()
28b58da2
FT
248 for tgt, off in zip(targets, offsets):
249 tgt.runcmd("show_text", "Offset: %f" % off)
c0897f29
FT
250 elif c == '.':
251 runcmd("frame_step")
252 paused = True
253 elif c == ',':
254 runcmd("frame_back_step")
255 paused = True
256
257with rawtty() as tty:
258 try:
259 main(tty)
260 except KeyboardInterrupt:
261 pass