2 # -*- coding: utf-8 -*-
4 """Tool to control VLC via remote network interface.
6 All excess args are sent as a one shot command.
9 from __future__ import print_function
23 def setup_logging(logfile=None, base=None, debug=False, stdout=False):
24 """Set up the logging module."""
25 fmt = '%(asctime)s: %(levelname)-7s: '
27 fmt += '%(filename)s:%(funcName)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
33 level = logging.DEBUG if debug else logging.INFO
36 handler = logging.StreamHandler(stream=sys.stdout)
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)
45 formatter = logging.Formatter(fmt, datefmt)
46 handler.setFormatter(formatter)
48 logger = logging.getLogger()
49 logger.addHandler(handler)
50 logger.setLevel(level)
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)
59 print('Connecting to %s:%s ... ' %(host, port), end='')
62 s.connect((host, port))
63 except socket.error as e:
64 logging.fatal('%s', e)
75 class VlcConsole(code.InteractiveConsole):
76 """VLC command line console."""
78 def __init__(self, *args, **kwargs):
79 self.server = kwargs.pop('server')
81 code.InteractiveConsole.__init__(self, *args, **kwargs)
83 def _drain_socket(self):
84 """Process any remaining status from the server."""
87 self.write(self.server.recv(1024 * 1024))
88 except socket.timeout:
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')
96 def _run_command(self, cmd, passthru=False):
97 """Run |cmd| and return its output."""
99 self._send_command(cmd)
103 result += self.server.recv(1024 * 1024)
104 except socket.timeout:
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)
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)
121 def _get_current_position(self):
122 """Get current position in the playback (in seconds).
127 return int(self._run_command('get_time').splitlines()[-1])
129 def _get_stracks(self):
130 """Get all available tracks.
133 +----[ Subtitle Track ]
135 | 3 - Track 1 - [English]
136 | 4 - Track 2 - [Arabic] *
137 | 5 - Track 3 - [Dutch]
138 +----[ end of Subtitle Track ]
142 for line in self._run_command('strack').splitlines():
143 if line.startswith('| '):
150 logging.debug('Current track: %s', current)
151 logging.debug('Found tracks: %r', tracks)
152 return (current, tracks)
154 def _get_current_volume(self):
155 """Return current volume.
158 status change: ( audio volume: 115 )
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)
164 return int(m.group(1))
167 def _custom_getparser_help(self, parser):
168 parser.add_argument('command', nargs='?')
171 # def _custom_help(self, opts):
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')
183 def _custom_seek(self, opts):
184 """Implement relative seek.
186 VLC only supports absolute seeking.
188 OFFSETS = (0, 5, 15, 30, 60)
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':
196 position = current_position + offset
199 position = int(opts.position)
201 logging.error('seek takes an int, not "%s"', opts.position)
204 self._run_command('seek %s' % (position,), passthru=True)
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')
212 def _custom_strack(self, opts):
213 if opts.track == 'n':
215 current, tracks = self._get_stracks()
216 pos = tracks.index(current) + 1
217 if pos == len(tracks):
221 track = '' if opts.track is None else opts.track
222 self._run_command('strack %s' % (track,), passthru=True)
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 '
229 'set: Volume level to set (percentage)')
232 def _custom_vol(self, opts):
233 """Implement relative volume.
235 VLC only supports absolute volume.
237 vol = self._get_current_volume()
239 self.write('Unknown current volume :(\n')
241 if opts.direction == 'up':
243 elif opts.direction == 'down':
246 vol = int(256 * (opts.level / 100.0))
247 self._send_command('volume %s' % (vol,))
251 # 'help': _custom_help,
252 'seek': _custom_seek,
253 'strack': _custom_strack,
259 """List of commands the server knows about.
262 +----[ Remote control commands ]
264 | add XYZ . . . . . . . . . . . . add XYZ to playlist
266 | quit . . . . . . . . . . . . . . . . . . . quit vlc
271 return self._commands
273 #logging.debug('Help output: {{{%s}}}', text)
274 text = self._run_command('help')
276 for line in text.splitlines():
277 if line.startswith('| '):
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
285 def _completer(self, text, state):
286 """Complete |text|."""
287 start = readline.get_begidx()
289 # Only complete commands atm, not their args.
292 if state == 0 and text == '':
295 for i, command in enumerate(self.commands):
296 out += '%-20s' % command
297 if ((i + 1) % 4) == 0:
298 self.write(out + '\n')
301 self.write(out + '\n')
303 elif state or text == '':
307 for command in self.commands:
308 if command.startswith(text):
310 logging.debug('matches: %r', ret)
318 def completer(self, text, state):
319 """Complete |text|."""
321 logging.debug('completer: state:%s text:{%s}', state, text)
322 return self._completer(text, state)
324 logging.exception('completer failed:')
326 def init_console(self):
327 """Set up the command line interface."""
330 readline.parse_and_bind('tab: complete')
331 readline.set_completer(self.completer)
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)
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:])
345 logging.debug('custom command found: %s', custom_command)
346 parser = self._load_custom_parser(cmd[0])
348 opts = parser.parse_args(cmd[1:])
349 custom_command(self, opts)
354 self._send_command(source)
355 if source in ('logout', 'quit'):
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')
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)
378 with connection(opts.host, opts.port, quiet=bool(args)) as server:
379 console = VlcConsole(server=server)
381 console.push(' '.join(args))
383 console.init_console()
384 console.interact(banner='VLC interactive console; run "help".')
387 if __name__ == '__main__':
388 exit(main(sys.argv[1:]))