]> git.wh0rd.org Git - home.git/blob - .bin/vlc-rc
crostini-vapier-setup: helper for setting up new installs
[home.git] / .bin / vlc-rc
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 """Tool to control VLC via remote network interface.
5
6 All excess args are sent as a one shot command.
7 """
8
9 from __future__ import print_function
10
11 import argparse
12 import code
13 import contextlib
14 import logging
15 import os
16 import re
17 import readline
18 import socket
19 import sys
20 import time
21
22
23 def setup_logging(logfile=None, base=None, debug=False, stdout=False):
24     """Set up the logging module."""
25     fmt = '%(asctime)s: %(levelname)-7s: '
26     if debug:
27         fmt += '%(filename)s:%(funcName)s: '
28     fmt += '%(message)s'
29     # 'Sat, 05 Oct 2013 18:58:50 -0400 (EST)'
30     tzname = time.strftime('%Z', time.localtime())
31     datefmt = '%a, %d %b %Y %H:%M:%S ' + tzname
32
33     level = logging.DEBUG if debug else logging.INFO
34
35     if stdout is True:
36         handler = logging.StreamHandler(stream=sys.stdout)
37     else:
38         if logfile is None:
39             assert base
40             logfile = os.path.join(os.path.dirname(os.path.dirname(
41                 os.path.abspath(__file__))), 'logs', base)
42         handler = logging.handlers.RotatingFileHandler(
43             logfile, maxBytes=(10 * 1024 * 1024), backupCount=1)
44
45     formatter = logging.Formatter(fmt, datefmt)
46     handler.setFormatter(formatter)
47
48     logger = logging.getLogger()
49     logger.addHandler(handler)
50     logger.setLevel(level)
51
52
53 @contextlib.contextmanager
54 def connection(host, port, quiet=False):
55     """Connect to |host| on |port| and return the socket."""
56     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
57     try:
58         if not quiet:
59             print('Connecting to %s:%s ... ' %(host, port), end='')
60             sys.stdout.flush()
61         try:
62             s.connect((host, port))
63         except socket.error as e:
64             logging.fatal('%s', e)
65             sys.exit(1)
66         s.settimeout(0.1)
67         if not quiet:
68             print('connected!')
69         yield s
70     finally:
71         #s.shutdown()
72         s.close()
73
74
75 class VlcConsole(code.InteractiveConsole):
76     """VLC command line console."""
77
78     def __init__(self, *args, **kwargs):
79         self.server = kwargs.pop('server')
80         self._commands = []
81         code.InteractiveConsole.__init__(self, *args, **kwargs)
82
83     def _drain_socket(self):
84         """Process any remaining status from the server."""
85         try:
86             while True:
87                 self.write(self.server.recv(1024 * 1024))
88         except socket.timeout:
89             return
90
91     def _send_command(self, cmd):
92         """Send |cmd| to the server."""
93         logging.debug('Sending command: %s', cmd)
94         self.server.send(cmd + '\n')
95
96     def _run_command(self, cmd, passthru=False):
97         """Run |cmd| and return its output."""
98         self._drain_socket()
99         self._send_command(cmd)
100         result = ''
101         while True:
102             try:
103                 result += self.server.recv(1024 * 1024)
104             except socket.timeout:
105                 if '\n' in result:
106                     break
107         if passthru:
108             self.write(result)
109         return result
110
111     def _load_custom_parser(self, cmd):
112         """Find custom parser for |cmd| as needed."""
113         parser_key = '_custom_parser_%s' % (cmd,)
114         parser = getattr(self, parser_key, None)
115         if parser is None:
116             new_parser = argparse.ArgumentParser(prog=cmd, add_help=False)
117             parser = getattr(self, '_custom_getparser_%s' % (cmd,))(new_parser)
118             setattr(self, parser_key, parser)
119         return parser
120
121     def _get_current_position(self):
122         """Get current position in the playback (in seconds).
123
124         Example output:
125         1234
126         """
127         return int(self._run_command('get_time').splitlines()[-1])
128
129     def _get_stracks(self):
130         """Get all available tracks.
131
132         Example output:
133         +----[ Subtitle Track ]
134         | -1 - Disable
135         | 3 - Track 1 - [English]
136         | 4 - Track 2 - [Arabic] *
137         | 5 - Track 3 - [Dutch]
138         +----[ end of Subtitle Track ]
139         """
140         current = '-1'
141         tracks = []
142         for line in self._run_command('strack').splitlines():
143             if line.startswith('| '):
144                 split = line.split()
145                 if len(split) > 1:
146                     track = split[1]
147                     tracks.append(track)
148                     if split[-1] == '*':
149                         current = track
150         logging.debug('Current track: %s', current)
151         logging.debug('Found tracks: %r', tracks)
152         return (current, tracks)
153
154     def _get_current_volume(self):
155         """Return current volume.
156
157         Example output:
158         status change: ( audio volume: 115 )
159         """
160         vol_re = re.compile(r'^status change: \( audio volume: ([0-9]+) \)')
161         for line in self._run_command('volume').splitlines():
162             m = vol_re.match(line)
163             if m:
164                 return int(m.group(1))
165         return None
166
167     def _custom_getparser_help(self, parser):
168         parser.add_argument('command', nargs='?')
169         return parser
170
171 #    def _custom_help(self, opts):
172 #        if opts.command:
173 #            
174 #        else:
175 #            
176
177     def _custom_getparser_seek(self, parser):
178         parser.add_argument('position',
179                             help='Either a time in seconds, '
180                                  'or "b" for back or "f" for forward')
181         return parser
182
183     def _custom_seek(self, opts):
184         """Implement relative seek.
185
186         VLC only supports absolute seeking.
187         """
188         OFFSETS = (0, 5, 15, 30, 60)
189
190         if (opts.position.replace('b', '') == '' or
191             opts.position.replace('f', '') == ''):
192             current_position = self._get_current_position()
193             offset = OFFSETS[len(opts.position)]
194             if opts.position[0] == 'b':
195                 offset *= -1
196             position = current_position + offset
197         else:
198             try:
199                 position = int(opts.position)
200             except ValueError:
201                 logging.error('seek takes an int, not "%s"', opts.position)
202                 return
203
204         self._run_command('seek %s' % (position,), passthru=True)
205
206     def _custom_getparser_strack(self, parser):
207         parser.add_argument('track', nargs='?',
208                             help='Use "n" for next track, '
209                                  'or a number to select')
210         return parser
211
212     def _custom_strack(self, opts):
213         if opts.track == 'n':
214             # Select next track.
215             current, tracks = self._get_stracks()
216             pos = tracks.index(current) + 1
217             if pos == len(tracks):
218                 pos = 0
219             track = tracks[pos]
220         else:
221             track = '' if opts.track is None else opts.track
222         self._run_command('strack %s' % (track,), passthru=True)
223
224     def _custom_getparser_vol(self, parser):
225         parser.add_argument('direction', choices=('down', 'up', 'set'))
226         parser.add_argument('level', nargs='?', default='10', type=int,
227                             help='{down,up}: Volume level to adjust '
228                                  '(256 is 100%); '
229                                  'set: Volume level to set (percentage)')
230         return parser
231
232     def _custom_vol(self, opts):
233         """Implement relative volume.
234
235         VLC only supports absolute volume.
236         """
237         vol = self._get_current_volume()
238         if vol is None:
239             self.write('Unknown current volume :(\n')
240             return
241         if opts.direction == 'up':
242             vol += opts.level
243         elif opts.direction == 'down':
244             vol -= opts.level
245         else:
246             vol = int(256 * (opts.level / 100.0))
247         self._send_command('volume %s' % (vol,))
248
249     CUSTOM_COMMANDS = {
250         'fullscreen': 'f',
251 #        'help': _custom_help,
252         'seek': _custom_seek,
253         'strack': _custom_strack,
254         'vol': _custom_vol,
255     }
256
257     @property
258     def commands(self):
259         """List of commands the server knows about.
260
261         Example format:
262         +----[ Remote control commands ]
263         |
264         | add XYZ  . . . . . . . . . . . . add XYZ to playlist
265         ....
266         | quit . . . . . . . . . . . . . . . . . . .  quit vlc
267         |
268         +----[ end of help ]
269         """
270         if self._commands:
271             return self._commands
272
273         #logging.debug('Help output: {{{%s}}}', text)
274         text = self._run_command('help')
275         commands = []
276         for line in text.splitlines():
277             if line.startswith('| '):
278                 split = line.split()
279                 if len(split) > 1:
280                     commands.append(split[1].rstrip('.'))
281         logging.debug('parsed commands: %s', commands)
282         self._commands = sorted(set(commands + self.CUSTOM_COMMANDS.keys()))
283         return self._commands
284
285     def _completer(self, text, state):
286         """Complete |text|."""
287         start = readline.get_begidx()
288         if start != 0:
289             # Only complete commands atm, not their args.
290             return None
291
292         if state == 0 and text == '':
293             self.write('\n')
294             out = ''
295             for i, command in enumerate(self.commands):
296                 out += '%-20s' % command
297                 if ((i + 1) % 4) == 0:
298                     self.write(out + '\n')
299                     out = ''
300             if out:
301                 self.write(out + '\n')
302             return None
303         elif state or text == '':
304             return None
305
306         ret = []
307         for command in self.commands:
308             if command.startswith(text):
309                 ret.append(command)
310         logging.debug('matches: %r', ret)
311         if not ret:
312             return None
313         elif len(ret) == 1:
314             return ret[0]
315         else:
316             return None
317
318     def completer(self, text, state):
319         """Complete |text|."""
320         try:
321             logging.debug('completer: state:%s text:{%s}', state, text)
322             return self._completer(text, state)
323         except Exception:
324             logging.exception('completer failed:')
325
326     def init_console(self):
327         """Set up the command line interface."""
328         sys.ps1 = 'vlc-rc$ '
329         sys.ps2 = '> '
330         readline.parse_and_bind('tab: complete')
331         readline.set_completer(self.completer)
332
333     def runsource(self, source, filename=None, symbol=None):
334         """Run the |source| (vlc command) the user wants."""
335         logging.debug('source=%r, filename=%r, symbol=%r',
336                       source, filename, symbol)
337         if source:
338             cmd = source.split()
339             custom_command = self.CUSTOM_COMMANDS.get(cmd[0])
340             if not custom_command is None:
341                 if isinstance(custom_command, str):
342                     logging.debug('command alias found: %s', custom_command)
343                     source = ' '.join([custom_command] + cmd[1:])
344                 else:
345                     logging.debug('custom command found: %s', custom_command)
346                     parser = self._load_custom_parser(cmd[0])
347                     try:
348                         opts = parser.parse_args(cmd[1:])
349                         custom_command(self, opts)
350                     except SystemExit:
351                         pass
352                     return
353
354             self._send_command(source)
355             if source in ('logout', 'quit'):
356                 sys.exit(0)
357         self._drain_socket()
358
359
360 def get_parser():
361     """Get a command line parser."""
362     parser = argparse.ArgumentParser(description=__doc__)
363     parser.add_argument('-H', '--host', default='vapier',
364                         help='Server to talk to')
365     parser.add_argument('-p', '--port', default=8001,
366                         help='Port to connect to')
367     parser.add_argument('--debug', default=False, action='store_true',
368                         help='Enable debug mode')
369     return parser
370
371
372 def main(argv):
373     """The main script entry point"""
374     parser = get_parser()
375     (opts, args) = parser.parse_known_args(argv)
376     setup_logging(debug=opts.debug, stdout=True)
377
378     with connection(opts.host, opts.port, quiet=bool(args)) as server:
379         console = VlcConsole(server=server)
380         if args:
381             console.push(' '.join(args))
382         else:
383             console.init_console()
384             console.interact(banner='VLC interactive console; run "help".')
385
386
387 if __name__ == '__main__':
388     exit(main(sys.argv[1:]))