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