]> git.wh0rd.org - home.git/blame - .bin/vlc-rc
cros-board: update
[home.git] / .bin / vlc-rc
CommitLineData
bc123377
MF
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4"""Tool to control VLC via remote network interface.
5
6All excess args are sent as a one shot command.
7"""
8
9from __future__ import print_function
10
11import argparse
12import code
13import contextlib
14import logging
15import os
16import re
17import readline
18import socket
19import sys
20import time
21
22
23def 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
54def 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:
8250a409
MF
59 print('Connecting to %s:%s ... ' %(host, port), end='')
60 sys.stdout.flush()
bc123377
MF
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
75class 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:
8250a409 105 if '\n' in result:
bc123377
MF
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:
8250a409
MF
198 try:
199 position = int(opts.position)
200 except ValueError:
201 logging.error('seek takes an int, not "%s"', opts.position)
202 return
bc123377
MF
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
360def 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
372def 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
387if __name__ == '__main__':
388 exit(main(sys.argv[1:]))