Dotfiles
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

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