From bc123377a5d6360085e1ce7569177fbde385da0b Mon Sep 17 00:00:00 2001 From: Mike Frysinger Date: Mon, 10 Oct 2016 00:53:52 -0400 Subject: [PATCH] vlc-rc: rewrite in python --- .bin/vlc-rc | 477 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 383 insertions(+), 94 deletions(-) diff --git a/.bin/vlc-rc b/.bin/vlc-rc index 04a48d2..ef6bb1e 100755 --- a/.bin/vlc-rc +++ b/.bin/vlc-rc @@ -1,94 +1,383 @@ -#!/bin/bash - -exec 3<> /dev/tcp/vapier/8001 -send() { echo "$*" 1>&3 ; } -recv() { local l; read l <&3; echo ${l%$'\r'} | sed 's:\r$::' ; } - -shell() { - cat <&3 & - trap "kill $!" 0 - send "${*:-help}" - sleep 1 - while read -p "vlc$ " -e l ; do - [[ -z $l ]] && continue - case $l in - "?") l="help" ;; - esac - send "$l" - case $l in - quit) break ;; - esac - sleep 1 - done -} - -case $1 in -f|fullscreen) - send f $2 - ;; -pause|quit|next|prev) - send $1 - ;; -vol) - shift - send volume - curr=$(recv) - lvl=${2:-10} - case ${curr} in - # status change: ( audio volume: 115 ) - "status change"*"audio volume"*) - vol=$(echo "${curr}" | awk '{ print $(NF-1) }') - case $1 in - up) : $(( vol += lvl )) ;; - down) : $(( vol -= lvl )) ;; - esac - send volume ${vol} - ;; - esac - ;; -seek) - if [[ -n $2 && -z ${2//[-+]} ]] ; then - off=( 0 5 15 30 60 ) - off=${off[${#2}]} - send get_time - curr=$(recv) - set -- seek $(( curr ${2:0:1} off )) - send "$@" - else - shell "$@" - fi - ;; -strack) - if [[ $2 == "n" ]] ; then - set -f - send strack - e="+----[ end of Subtitles Track ]" - while l=$(recv) ; do - set -- $l - if [[ $l == "$e" ]] ; then - break - elif [[ $l == "| "* ]] ; then - [[ -z $s ]] && s=$2 - if [[ $l == *" *" ]] ; then - l=$(recv) - if [[ $l != "$e" ]] ; then - set -- $l - s=$2 - fi - break - fi - fi - done - kdialog --msgbox "$l" & - set -- strack $s - send "$@" - else - shell "$@" - fi - ;; -*) - shell "$@" - ;; -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='') + 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 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: + position = opts.position + + 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:])) -- 2.39.5