]> git.wh0rd.org - home.git/commitdiff
vlc-rc: rewrite in python
authorMike Frysinger <vapier@gentoo.org>
Mon, 10 Oct 2016 04:53:52 +0000 (00:53 -0400)
committerMike Frysinger <vapier@gentoo.org>
Mon, 10 Oct 2016 04:53:52 +0000 (00:53 -0400)
.bin/vlc-rc

index 04a48d20d8a631e732a508ff81f6750ff2c0926b..ef6bb1e7cad307befab4f3c500655c2ddb3299a6 100755 (executable)
-#!/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:]))