You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
170 lines
5.6 KiB
170 lines
5.6 KiB
#!/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) |
|
|
|
|