X-Git-Url: https://git.wh0rd.org/?a=blobdiff_plain;f=.bin%2Fvlc-rc;h=f2d0d617c89aeb4ab9f876e5b4e208c5292423ba;hb=HEAD;hp=f52e6cf46497375be76c1276dd3647ed5cad8c4f;hpb=5f25a449b106bc1c35d5930b925dae337d41ef22;p=home.git diff --git a/.bin/vlc-rc b/.bin/vlc-rc index f52e6cf..f2d0d61 100755 --- a/.bin/vlc-rc +++ b/.bin/vlc-rc @@ -1,35 +1,388 @@ -#!/bin/bash - -exec 3<> /dev/tcp/vapier/8001 -send() { echo "$*" 1>&3 ; } -recv() { local l; read l <&3; echo ${l%$'\r'} ; } - -case $1 in -f|fullscreen) - send f $2 - ;; -help) - send $1 - cat <&3 - ;; -pause|quit) - send $1 - ;; -vol) - shift - send vol$* - ;; -seek) - off=( 0 5 15 30 60 ) - [[ -z $2 || -n ${2//[-+]} ]] && exit 0 - off=${off[${#2}]} - send get_time - curr=$(recv) - send seek $(( curr ${2:0:1} off )) - ;; -*) - send "$@" - ;; -esac - -exit 0 +#!/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:]))