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