diff options
-rw-r--r-- | mpvc/README.md | 14 | ||||
-rwxr-xr-x | mpvc/mpvc | 187 |
2 files changed, 201 insertions, 0 deletions
diff --git a/mpvc/README.md b/mpvc/README.md new file mode 100644 index 0000000..7324b57 --- /dev/null +++ b/mpvc/README.md @@ -0,0 +1,14 @@ +# mpvc: remote control for a long-running mpv process + +This is what I use as a media player nowadays. +To launch the long-running mpv, run `mpvc launch_mpv`. +Typically I do that from a runit service, so the mpv runs from startup +to shutdown. + +Commands are poorly documented. I use `click` for building the +interface, so it is at least fairly discoverable. + +# Dependencies + +* Python 3.6+ +* [click](http://click.pocoo.org/). This is widely available in distros and ports trees. See its [page on repology.org](https://repology.org/project/python:click/versions) if you need help finding it. diff --git a/mpvc/mpvc b/mpvc/mpvc new file mode 100755 index 0000000..1f29a54 --- /dev/null +++ b/mpvc/mpvc @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +import io +import json +import os +import socket +import sys +import click + + +MPV_SOCKET_PATH = "/tmp/mpvd" +NEWLINE = b"\n"[0] + + +def terminate_on_failure(response): + if response["error"] != "success": + sys.stderr.write(f"{response['error']}") + sys.stderr.flush() + sys.exit(1) + + +def open_socket(): + the_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0) + try: + the_socket.connect(MPV_SOCKET_PATH) + return the_socket + except OSError: + the_socket.close() + raise + + +def snarf_available(the_socket): + buffer = io.BytesIO() + received = [] + while True: + chunk = the_socket.recv(65536) + buffer.write(chunk) + if chunk[-1] != NEWLINE: + continue + buffer.seek(0) + received = [json.loads(x) for x in buffer] + buffer.seek(0) + buffer.truncate() + received = [x for x in received if "request_id" in x] + if not received: + continue + return received[0] + + return {"error": "no data"} + + +def send_command(command, *args): + command_json = ( + json.dumps({"command": [command] + list(args)}).encode("UTF-8") + b"\n" + ) + with open_socket() as the_socket: + the_socket.send(command_json) + return snarf_available(the_socket) + + +def set_property(property_name, value): + return send_command("set_property", property_name, value) + + +def update_playlist_helper(opt, args): + if not args: + args = [x.rstrip("\n") for x in sys.stdin] + + for entry in args: + terminate_on_failure(send_command("loadfile", entry, opt)) + + +@click.group() +def cli(): + pass + + +def get_property(property_name): + return send_command("get_property", property_name) + + +@cli.command(help="Show an mpv property.") +@click.argument("property_name") +def prop(property_name): + resp = get_property(property_name) + terminate_on_failure(resp) + print(resp["data"]) + + +@cli.command(help="Set or show the volume.") +@click.argument("new_volume", type=float, required=False, default=None) +def volume(new_volume): + if new_volume is not None: + terminate_on_failure(set_property("volume", new_volume)) + else: + current_volume = get_property("volume") + terminate_on_failure(current_volume) + print(f"Current volume: {current_volume['data']}") + + +@cli.command(help="Stop playback.") +def stop(): + terminate_on_failure(send_command("stop")) + + +@cli.command(name="next", help="Jump to the next item in the playlist.") +def playlist_next(): + terminate_on_failure(send_command("playlist-next")) + + +@cli.command(name="prev", help="Jump to the previous item in the playlist.") +def playlist_prev(): + terminate_on_failure(send_command("playlist-prev")) + + +@cli.command(name="shuffle", help="Shuffle the playlist.") +def playlist_shuffle(): + terminate_on_failure(send_command("playlist-shuffle")) + + +@cli.command(name="clear", help="Clear the playlist.") +def playlist_clear(): + terminate_on_failure(send_command("playlist-clear")) + + +@cli.command(help="Jump to a specific entry in playlist, or resume playback.") +@click.argument("playlist_position", required=False, type=int, default=None) +def play(playlist_position): + if playlist_position is not None: + terminate_on_failure(set_property("playlist-pos-1", playlist_position)) + terminate_on_failure(set_property("pause", False)) + + +@cli.command(help="Pause playback.") +def pause(): + terminate_on_failure(set_property("pause", True)) + + +@cli.command(name="list", help="Print the playlist.") +def cmd_list(): + resp = send_command("get_property", "playlist") + terminate_on_failure(resp) + for entry in resp["data"]: + char_current = "*" if entry.get("current", None) else " " + char_playing = "!" if entry.get("playing", None) else " " + print(f"{char_playing}{char_current}{entry['filename']}") + + +@cli.command(help="Start a standalone mpv.") +def launch_mpv(): + MPV_NO_VIDEO = ("--no-video",) + MPV_COMMAND = ( + "mpv", + "--idle=yes", + "--no-terminal", + f"--input-ipc-server={MPV_SOCKET_PATH}", + ) + MPV_NO_VIDEO + os.execvp("mpv", MPV_COMMAND) + + +@cli.command(name="add", help="Append one or more items to the playlist.") +@click.argument("items", nargs=-1) +def append_to_playlist(items): + update_playlist_helper("append-play", items) + + +@cli.command(name="replace", help="Replace the playlist with the given arguments.") +@click.argument("items", nargs=-1) +def replace_playlist(items): + update_playlist_helper("replace", items) + + +@cli.command(context_settings=dict(ignore_unknown_options=True)) +@click.argument("amount", type=click.FLOAT) +@click.option("--absolute/--relative", default=False) +@click.option("--percent", is_flag=True, default=False) +def seek(amount, absolute, percent): + flag_words = { + (False, False): "relative", + (False, True): "relative-percent", + (True, False): "absolute", + (True, True): "absolute-percent", + } + terminate_on_failure(send_command("seek", amount, flag_words[(absolute, percent)])) + + +if __name__ == "__main__": + cli() |