-#!/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:]))