#!/usr/bin/python # -*- coding: utf-8 -*- """Tool to control VLC via remote network interface. All excess args are sent as a one shot command. """ from __future__ import print_function import argparse import code import contextlib import logging import os import re import readline import socket import sys import time def setup_logging(logfile=None, base=None, debug=False, stdout=False): """Set up the logging module.""" fmt = '%(asctime)s: %(levelname)-7s: ' if debug: fmt += '%(filename)s:%(funcName)s: ' fmt += '%(message)s' # 'Sat, 05 Oct 2013 18:58:50 -0400 (EST)' tzname = time.strftime('%Z', time.localtime()) datefmt = '%a, %d %b %Y %H:%M:%S ' + tzname level = logging.DEBUG if debug else logging.INFO if stdout is True: handler = logging.StreamHandler(stream=sys.stdout) else: if logfile is None: assert base logfile = os.path.join(os.path.dirname(os.path.dirname( os.path.abspath(__file__))), 'logs', base) handler = logging.handlers.RotatingFileHandler( logfile, maxBytes=(10 * 1024 * 1024), backupCount=1) formatter = logging.Formatter(fmt, datefmt) handler.setFormatter(formatter) logger = logging.getLogger() logger.addHandler(handler) logger.setLevel(level) @contextlib.contextmanager def connection(host, port, quiet=False): """Connect to |host| on |port| and return the socket.""" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: if not quiet: print('Connecting to %s:%s ... ' %(host, port), end='') sys.stdout.flush() try: s.connect((host, port)) except socket.error as e: logging.fatal('%s', e) sys.exit(1) s.settimeout(0.1) if not quiet: print('connected!') yield s finally: #s.shutdown() s.close() class VlcConsole(code.InteractiveConsole): """VLC command line console.""" def __init__(self, *args, **kwargs): self.server = kwargs.pop('server') self._commands = [] code.InteractiveConsole.__init__(self, *args, **kwargs) def _drain_socket(self): """Process any remaining status from the server.""" try: while True: self.write(self.server.recv(1024 * 1024)) except socket.timeout: return def _send_command(self, cmd): """Send |cmd| to the server.""" logging.debug('Sending command: %s', cmd) self.server.send(cmd + '\n') def _run_command(self, cmd, passthru=False): """Run |cmd| and return its output.""" self._drain_socket() self._send_command(cmd) result = '' while True: try: result += self.server.recv(1024 * 1024) except socket.timeout: if '\n' in result: break if passthru: self.write(result) return result def _load_custom_parser(self, cmd): """Find custom parser for |cmd| as needed.""" parser_key = '_custom_parser_%s' % (cmd,) parser = getattr(self, parser_key, None) if parser is None: new_parser = argparse.ArgumentParser(prog=cmd, add_help=False) parser = getattr(self, '_custom_getparser_%s' % (cmd,))(new_parser) setattr(self, parser_key, parser) return parser def _get_current_position(self): """Get current position in the playback (in seconds). Example output: 1234 """ return int(self._run_command('get_time').splitlines()[-1]) def _get_stracks(self): """Get all available tracks. Example output: +----[ Subtitle Track ] | -1 - Disable | 3 - Track 1 - [English] | 4 - Track 2 - [Arabic] * | 5 - Track 3 - [Dutch] +----[ end of Subtitle Track ] """ current = '-1' tracks = [] for line in self._run_command('strack').splitlines(): if line.startswith('| '): split = line.split() if len(split) > 1: track = split[1] tracks.append(track) if split[-1] == '*': current = track logging.debug('Current track: %s', current) logging.debug('Found tracks: %r', tracks) return (current, tracks) def _get_current_volume(self): """Return current volume. Example output: status change: ( audio volume: 115 ) """ vol_re = re.compile(r'^status change: \( audio volume: ([0-9]+) \)') for line in self._run_command('volume').splitlines(): m = vol_re.match(line) if m: return int(m.group(1)) return None def _custom_getparser_help(self, parser): parser.add_argument('command', nargs='?') return parser # def _custom_help(self, opts): # if opts.command: # # else: # def _custom_getparser_seek(self, parser): parser.add_argument('position', help='Either a time in seconds, ' 'or "b" for back or "f" for forward') return parser def _custom_seek(self, opts): """Implement relative seek. VLC only supports absolute seeking. """ OFFSETS = (0, 5, 15, 30, 60) if (opts.position.replace('b', '') == '' or opts.position.replace('f', '') == ''): current_position = self._get_current_position() offset = OFFSETS[len(opts.position)] if opts.position[0] == 'b': offset *= -1 position = current_position + offset else: try: position = int(opts.position) except ValueError: logging.error('seek takes an int, not "%s"', opts.position) return self._run_command('seek %s' % (position,), passthru=True) def _custom_getparser_strack(self, parser): parser.add_argument('track', nargs='?', help='Use "n" for next track, ' 'or a number to select') return parser def _custom_strack(self, opts): if opts.track == 'n': # Select next track. current, tracks = self._get_stracks() pos = tracks.index(current) + 1 if pos == len(tracks): pos = 0 track = tracks[pos] else: track = '' if opts.track is None else opts.track self._run_command('strack %s' % (track,), passthru=True) def _custom_getparser_vol(self, parser): parser.add_argument('direction', choices=('down', 'up', 'set')) parser.add_argument('level', nargs='?', default='10', type=int, help='{down,up}: Volume level to adjust ' '(256 is 100%); ' 'set: Volume level to set (percentage)') return parser def _custom_vol(self, opts): """Implement relative volume. VLC only supports absolute volume. """ vol = self._get_current_volume() if vol is None: self.write('Unknown current volume :(\n') return if opts.direction == 'up': vol += opts.level elif opts.direction == 'down': vol -= opts.level else: vol = int(256 * (opts.level / 100.0)) self._send_command('volume %s' % (vol,)) CUSTOM_COMMANDS = { 'fullscreen': 'f', # 'help': _custom_help, 'seek': _custom_seek, 'strack': _custom_strack, 'vol': _custom_vol, } @property def commands(self): """List of commands the server knows about. Example format: +----[ Remote control commands ] | | add XYZ . . . . . . . . . . . . add XYZ to playlist .... | quit . . . . . . . . . . . . . . . . . . . quit vlc | +----[ end of help ] """ if self._commands: return self._commands #logging.debug('Help output: {{{%s}}}', text) text = self._run_command('help') commands = [] for line in text.splitlines(): if line.startswith('| '): split = line.split() if len(split) > 1: commands.append(split[1].rstrip('.')) logging.debug('parsed commands: %s', commands) self._commands = sorted(set(commands + self.CUSTOM_COMMANDS.keys())) return self._commands def _completer(self, text, state): """Complete |text|.""" start = readline.get_begidx() if start != 0: # Only complete commands atm, not their args. return None if state == 0 and text == '': self.write('\n') out = '' for i, command in enumerate(self.commands): out += '%-20s' % command if ((i + 1) % 4) == 0: self.write(out + '\n') out = '' if out: self.write(out + '\n') return None elif state or text == '': return None ret = [] for command in self.commands: if command.startswith(text): ret.append(command) logging.debug('matches: %r', ret) if not ret: return None elif len(ret) == 1: return ret[0] else: return None def completer(self, text, state): """Complete |text|.""" try: logging.debug('completer: state:%s text:{%s}', state, text) return self._completer(text, state) except Exception: logging.exception('completer failed:') def init_console(self): """Set up the command line interface.""" sys.ps1 = 'vlc-rc$ ' sys.ps2 = '> ' readline.parse_and_bind('tab: complete') readline.set_completer(self.completer) def runsource(self, source, filename=None, symbol=None): """Run the |source| (vlc command) the user wants.""" logging.debug('source=%r, filename=%r, symbol=%r', source, filename, symbol) if source: cmd = source.split() custom_command = self.CUSTOM_COMMANDS.get(cmd[0]) if not custom_command is None: if isinstance(custom_command, str): logging.debug('command alias found: %s', custom_command) source = ' '.join([custom_command] + cmd[1:]) else: logging.debug('custom command found: %s', custom_command) parser = self._load_custom_parser(cmd[0]) try: opts = parser.parse_args(cmd[1:]) custom_command(self, opts) except SystemExit: pass return self._send_command(source) if source in ('logout', 'quit'): sys.exit(0) self._drain_socket() def get_parser(): """Get a command line parser.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('-H', '--host', default='vapier', help='Server to talk to') parser.add_argument('-p', '--port', default=8001, help='Port to connect to') parser.add_argument('--debug', default=False, action='store_true', help='Enable debug mode') return parser def main(argv): """The main script entry point""" parser = get_parser() (opts, args) = parser.parse_known_args(argv) setup_logging(debug=opts.debug, stdout=True) with connection(opts.host, opts.port, quiet=bool(args)) as server: console = VlcConsole(server=server) if args: console.push(' '.join(args)) else: console.init_console() console.interact(banner='VLC interactive console; run "help".') if __name__ == '__main__': exit(main(sys.argv[1:]))