#!/usr/bin/env python3 # Use ncmpcpp to play and control a mpd-httpd stream while still being able to control the local volume # (c) 2015 Taeyeon Mori # requires Python 3.4 or higher import os import sys import pty import tty import stat import array import fcntl import signal import asyncio import termios import argparse import contextlib import subprocess import concurrent.futures URL_TEMPLATE = "http://{host}:{http_port}/" # ASYNCIO HACKS ================================================================================== # asyncio is following a great rationale, but at this point in time, there are still some things # missing from it to make it truly useful. Here comes one of 'em: @asyncio.coroutine def async_stdio(loop=None): # get streams for the standard I/O (stdin and stdout) if not os.path.sameopenfile(0, 1): raise RuntimeError("The async_stdio hack only works when both STDIN and STDOUT point to the same TTY/PTS") if loop is None: loop = asyncio.get_event_loop() reader = asyncio.StreamReader() reader_protocol = asyncio.StreamReaderProtocol(reader) writer_transport, writer_protocol = yield from loop.connect_write_pipe(asyncio.streams.FlowControlMixin, os.fdopen(0, 'wb')) writer = asyncio.StreamWriter(writer_transport, writer_protocol, None, loop) yield from loop.connect_read_pipe(lambda: reader_protocol, sys.stdin) return reader, writer @asyncio.coroutine def async_pty(pty, loop=None): # same as above, just with a pty descriptor instead of STD* if loop is None: loop = asyncio.get_event_loop() reader = asyncio.StreamReader() reader_protocol = asyncio.StreamReaderProtocol(reader) writer_transport, writer_protocol = yield from loop.connect_write_pipe(asyncio.streams.FlowControlMixin, os.fdopen(pty, 'wb')) writer = asyncio.StreamWriter(writer_transport, writer_protocol, None, loop) yield from loop.connect_read_pipe(lambda: reader_protocol, os.fdopen(pty)) return reader, writer # END ASYNCIO HACKS ============================================================================== def parse_args(argv): parser = argparse.ArgumentParser(prog=argv[0], add_help=False) parser.add_argument("--help", action="store_true") parser.add_argument("-h", "--host", default=os.environ.get("MPD_HOST", "localhost")) parser.add_argument("-p", "--port", default=os.environ.get("MPD_PORT", "6600")) parser.add_argument("-P", "--http-port", default="8000") parser.add_argument("-E", "--http-output", default="1") args = parser.parse_args(argv[1:]) if args.help: parser.print_help() sys.exit(0) return args @asyncio.coroutine def main_coro(args, loop): # Enable stream subprocess.check_call(["mpc", "-h", args.host, "-p", args.port, "enable", args.http_output]) # Manage stdio std_reader, std_writer = yield from async_stdio() # Manage vlc vlc_proc = yield from asyncio.create_subprocess_exec("vlc", "--repeat", URL_TEMPLATE.format(**vars(args)), "-I", "rc", stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) vlc_writer = vlc_proc.stdin # Manage pty ptm, pts = pty.openpty() loop.add_signal_handler(signal.SIGWINCH, propagate_winsize, ptm) propagate_winsize(ptm) # Manage ncmpc ptm_reader, ptm_writer = yield from async_pty(ptm) pty_proc = yield from asyncio.create_subprocess_exec("ncmpcpp", "-h", args.host, "-p", args.port, stdin=pts, stdout=pts, stderr=pts, start_new_session=True, preexec_fn=reopen_tty) # Magic input_task = asyncio.async(process_input(std_reader, ptm_writer, vlc_writer)) output_task = asyncio.async(process_output(ptm_reader, std_writer)) yield from asyncio.wait([input_task, output_task, pty_proc.wait()], return_when=concurrent.futures.FIRST_COMPLETED) # Cleanup if pty_proc.returncode is None: pty_proc.terminate() vlc_proc.terminate() yield from asyncio.wait([input_task, output_task], timeout=1) loop.remove_signal_handler(signal.SIGWINCH) os.close(ptm) def propagate_winsize(fd): # Notify pty of window size buf = array.array('h', [0, 0, 0, 0]) fcntl.ioctl(1, termios.TIOCGWINSZ, buf, True) fcntl.ioctl(fd, termios.TIOCSWINSZ, buf) def reopen_tty(): # reopen tty to make it the controlling tty (see stdlib pty) open(os.ttyname(1), "wb").close() @asyncio.coroutine def process_input(reader, writer, vlc_writer): reader_fd = reader._transport.get_extra_info("pipe").fileno() if True: # placeholder for eventual context manager for termio reset tty.setraw(reader_fd) while True: data = yield from reader.read(512) if not data: break elif data == b'+': vlc_writer.write(b"volup 2\n") yield from vlc_writer.drain() elif data == b'-': vlc_writer.write(b"voldown 2\n") yield from vlc_writer.drain() #elif data == b'\x03': # CTRL-C, RAW mode # raise KeyboardInterrupt() else: writer.write(data) yield from writer.drain() @asyncio.coroutine def process_output(reader, writer): while True: data = yield from reader.read(256) if data: writer.write(data) yield from writer.drain() else: break if __name__ == "__main__": args = parse_args(sys.argv) loop = asyncio.get_event_loop() mode = termios.tcgetattr(0) try: sys.exit(loop.run_until_complete(main_coro(args, loop))) finally: termios.tcsetattr(0, termios.TCSADRAIN, mode)