]> git.wh0rd.org - home.git/blob - .bin/vlc-rc
cros-board: update
[home.git] / .bin / vlc-rc
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 """Tool to control VLC via remote network interface.
5
6 All excess args are sent as a one shot command.
7 """
8
9 from __future__ import print_function
10
11 import argparse
12 import code
13 import contextlib
14 import logging
15 import os
16 import re
17 import readline
18 import socket
19 import sys
20 import time
21
22
23 def setup_logging(logfile=None, base=None, debug=False, stdout=False):
24 """Set up the logging module."""
25 fmt = '%(asctime)s: %(levelname)-7s: '
26 if debug:
27 fmt += '%(filename)s:%(funcName)s: '
28 fmt += '%(message)s'
29 # 'Sat, 05 Oct 2013 18:58:50 -0400 (EST)'
30 tzname = time.strftime('%Z', time.localtime())
31 datefmt = '%a, %d %b %Y %H:%M:%S ' + tzname
32
33 level = logging.DEBUG if debug else logging.INFO
34
35 if stdout is True:
36 handler = logging.StreamHandler(stream=sys.stdout)
37 else:
38 if logfile is None:
39 assert base
40 logfile = os.path.join(os.path.dirname(os.path.dirname(
41 os.path.abspath(__file__))), 'logs', base)
42 handler = logging.handlers.RotatingFileHandler(
43 logfile, maxBytes=(10 * 1024 * 1024), backupCount=1)
44
45 formatter = logging.Formatter(fmt, datefmt)
46 handler.setFormatter(formatter)
47
48 logger = logging.getLogger()
49 logger.addHandler(handler)
50 logger.setLevel(level)
51
52
53 @contextlib.contextmanager
54 def connection(host, port, quiet=False):
55 """Connect to |host| on |port| and return the socket."""
56 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
57 try:
58 if not quiet:
59 print('Connecting to %s:%s ... ' %(host, port), end='')
60 sys.stdout.flush()
61 try:
62 s.connect((host, port))
63 except socket.error as e:
64 logging.fatal('%s', e)
65 sys.exit(1)
66 s.settimeout(0.1)
67 if not quiet:
68 print('connected!')
69 yield s
70 finally:
71 #s.shutdown()
72 s.close()
73
74
75 class VlcConsole(code.InteractiveConsole):
76 """VLC command line console."""
77
78 def __init__(self, *args, **kwargs):
79 self.server = kwargs.pop('server')
80 self._commands = []
81 code.InteractiveConsole.__init__(self, *args, **kwargs)
82
83 def _drain_socket(self):
84 """Process any remaining status from the server."""
85 try:
86 while True:
87 self.write(self.server.recv(1024 * 1024))
88 except socket.timeout:
89 return
90
91 def _send_command(self, cmd):
92 """Send |cmd| to the server."""
93 logging.debug('Sending command: %s', cmd)
94 self.server.send(cmd + '\n')
95
96 def _run_command(self, cmd, passthru=False):
97 """Run |cmd| and return its output."""
98 self._drain_socket()
99 self._send_command(cmd)
100 result = ''
101 while True:
102 try:
103 result += self.server.recv(1024 * 1024)
104 except socket.timeout:
105 if '\n' in result:
106 break
107 if passthru:
108 self.write(result)
109 return result
110
111 def _load_custom_parser(self, cmd):
112 """Find custom parser for |cmd| as needed."""
113 parser_key = '_custom_parser_%s' % (cmd,)
114 parser = getattr(self, parser_key, None)
115 if parser is None:
116 new_parser = argparse.ArgumentParser(prog=cmd, add_help=False)
117 parser = getattr(self, '_custom_getparser_%s' % (cmd,))(new_parser)
118 setattr(self, parser_key, parser)
119 return parser
120
121 def _get_current_position(self):
122 """Get current position in the playback (in seconds).
123
124 Example output:
125 1234
126 """
127 return int(self._run_command('get_time').splitlines()[-1])
128
129 def _get_stracks(self):
130 """Get all available tracks.
131
132 Example output:
133 +----[ Subtitle Track ]
134 | -1 - Disable
135 | 3 - Track 1 - [English]
136 | 4 - Track 2 - [Arabic] *
137 | 5 - Track 3 - [Dutch]
138 +----[ end of Subtitle Track ]
139 """
140 current = '-1'
141 tracks = []
142 for line in self._run_command('strack').splitlines():
143 if line.startswith('| '):
144 split = line.split()
145 if len(split) > 1:
146 track = split[1]
147 tracks.append(track)
148 if split[-1] == '*':
149 current = track
150 logging.debug('Current track: %s', current)
151 logging.debug('Found tracks: %r', tracks)
152 return (current, tracks)
153
154 def _get_current_volume(self):
155 """Return current volume.
156
157 Example output:
158 status change: ( audio volume: 115 )
159 """
160 vol_re = re.compile(r'^status change: \( audio volume: ([0-9]+) \)')
161 for line in self._run_command('volume').splitlines():
162 m = vol_re.match(line)
163 if m:
164 return int(m.group(1))
165 return None
166
167 def _custom_getparser_help(self, parser):
168 parser.add_argument('command', nargs='?')
169 return parser
170
171 # def _custom_help(self, opts):
172 # if opts.command:
173 #
174 # else:
175 #
176
177 def _custom_getparser_seek(self, parser):
178 parser.add_argument('position',
179 help='Either a time in seconds, '
180 'or "b" for back or "f" for forward')
181 return parser
182
183 def _custom_seek(self, opts):
184 """Implement relative seek.
185
186 VLC only supports absolute seeking.
187 """
188 OFFSETS = (0, 5, 15, 30, 60)
189
190 if (opts.position.replace('b', '') == '' or
191 opts.position.replace('f', '') == ''):
192 current_position = self._get_current_position()
193 offset = OFFSETS[len(opts.position)]
194 if opts.position[0] == 'b':
195 offset *= -1
196 position = current_position + offset
197 else:
198 try:
199 position = int(opts.position)
200 except ValueError:
201 logging.error('seek takes an int, not "%s"', opts.position)
202 return
203
204 self._run_command('seek %s' % (position,), passthru=True)
205
206 def _custom_getparser_strack(self, parser):
207 parser.add_argument('track', nargs='?',
208 help='Use "n" for next track, '
209 'or a number to select')
210 return parser
211
212 def _custom_strack(self, opts):
213 if opts.track == 'n':
214 # Select next track.
215 current, tracks = self._get_stracks()
216 pos = tracks.index(current) + 1
217 if pos == len(tracks):
218 pos = 0
219 track = tracks[pos]
220 else:
221 track = '' if opts.track is None else opts.track
222 self._run_command('strack %s' % (track,), passthru=True)
223
224 def _custom_getparser_vol(self, parser):
225 parser.add_argument('direction', choices=('down', 'up', 'set'))
226 parser.add_argument('level', nargs='?', default='10', type=int,
227 help='{down,up}: Volume level to adjust '
228 '(256 is 100%); '
229 'set: Volume level to set (percentage)')
230 return parser
231
232 def _custom_vol(self, opts):
233 """Implement relative volume.
234
235 VLC only supports absolute volume.
236 """
237 vol = self._get_current_volume()
238 if vol is None:
239 self.write('Unknown current volume :(\n')
240 return
241 if opts.direction == 'up':
242 vol += opts.level
243 elif opts.direction == 'down':
244 vol -= opts.level
245 else:
246 vol = int(256 * (opts.level / 100.0))
247 self._send_command('volume %s' % (vol,))
248
249 CUSTOM_COMMANDS = {
250 'fullscreen': 'f',
251 # 'help': _custom_help,
252 'seek': _custom_seek,
253 'strack': _custom_strack,
254 'vol': _custom_vol,
255 }
256
257 @property
258 def commands(self):
259 """List of commands the server knows about.
260
261 Example format:
262 +----[ Remote control commands ]
263 |
264 | add XYZ . . . . . . . . . . . . add XYZ to playlist
265 ....
266 | quit . . . . . . . . . . . . . . . . . . . quit vlc
267 |
268 +----[ end of help ]
269 """
270 if self._commands:
271 return self._commands
272
273 #logging.debug('Help output: {{{%s}}}', text)
274 text = self._run_command('help')
275 commands = []
276 for line in text.splitlines():
277 if line.startswith('| '):
278 split = line.split()
279 if len(split) > 1:
280 commands.append(split[1].rstrip('.'))
281 logging.debug('parsed commands: %s', commands)
282 self._commands = sorted(set(commands + self.CUSTOM_COMMANDS.keys()))
283 return self._commands
284
285 def _completer(self, text, state):
286 """Complete |text|."""
287 start = readline.get_begidx()
288 if start != 0:
289 # Only complete commands atm, not their args.
290 return None
291
292 if state == 0 and text == '':
293 self.write('\n')
294 out = ''
295 for i, command in enumerate(self.commands):
296 out += '%-20s' % command
297 if ((i + 1) % 4) == 0:
298 self.write(out + '\n')
299 out = ''
300 if out:
301 self.write(out + '\n')
302 return None
303 elif state or text == '':
304 return None
305
306 ret = []
307 for command in self.commands:
308 if command.startswith(text):
309 ret.append(command)
310 logging.debug('matches: %r', ret)
311 if not ret:
312 return None
313 elif len(ret) == 1:
314 return ret[0]
315 else:
316 return None
317
318 def completer(self, text, state):
319 """Complete |text|."""
320 try:
321 logging.debug('completer: state:%s text:{%s}', state, text)
322 return self._completer(text, state)
323 except Exception:
324 logging.exception('completer failed:')
325
326 def init_console(self):
327 """Set up the command line interface."""
328 sys.ps1 = 'vlc-rc$ '
329 sys.ps2 = '> '
330 readline.parse_and_bind('tab: complete')
331 readline.set_completer(self.completer)
332
333 def runsource(self, source, filename=None, symbol=None):
334 """Run the |source| (vlc command) the user wants."""
335 logging.debug('source=%r, filename=%r, symbol=%r',
336 source, filename, symbol)
337 if source:
338 cmd = source.split()
339 custom_command = self.CUSTOM_COMMANDS.get(cmd[0])
340 if not custom_command is None:
341 if isinstance(custom_command, str):
342 logging.debug('command alias found: %s', custom_command)
343 source = ' '.join([custom_command] + cmd[1:])
344 else:
345 logging.debug('custom command found: %s', custom_command)
346 parser = self._load_custom_parser(cmd[0])
347 try:
348 opts = parser.parse_args(cmd[1:])
349 custom_command(self, opts)
350 except SystemExit:
351 pass
352 return
353
354 self._send_command(source)
355 if source in ('logout', 'quit'):
356 sys.exit(0)
357 self._drain_socket()
358
359
360 def get_parser():
361 """Get a command line parser."""
362 parser = argparse.ArgumentParser(description=__doc__)
363 parser.add_argument('-H', '--host', default='vapier',
364 help='Server to talk to')
365 parser.add_argument('-p', '--port', default=8001,
366 help='Port to connect to')
367 parser.add_argument('--debug', default=False, action='store_true',
368 help='Enable debug mode')
369 return parser
370
371
372 def main(argv):
373 """The main script entry point"""
374 parser = get_parser()
375 (opts, args) = parser.parse_known_args(argv)
376 setup_logging(debug=opts.debug, stdout=True)
377
378 with connection(opts.host, opts.port, quiet=bool(args)) as server:
379 console = VlcConsole(server=server)
380 if args:
381 console.push(' '.join(args))
382 else:
383 console.init_console()
384 console.interact(banner='VLC interactive console; run "help".')
385
386
387 if __name__ == '__main__':
388 exit(main(sys.argv[1:]))