diff --git a/app.py b/app.py index 347d419..d2ca923 100644 --- a/app.py +++ b/app.py @@ -1,3 +1,4 @@ +from asyncio.streams import StreamReader, StreamWriter import re from typing import Any, Mapping from flask import Flask, render_template, request, send_file, jsonify @@ -6,9 +7,9 @@ import subprocess import signal from pathlib import Path import tempfile -import socket import json -import time +import asyncio + mpv_pidfile = Path(tempfile.gettempdir()).joinpath("laas_mpv.pidfile") mpv_socket = Path(tempfile.gettempdir()).joinpath("mpvsocket") @@ -34,7 +35,7 @@ def cleanup_unclean(): def shutdown(status: int = 0): - playback_stop() + mpv_stop() os._exit(status) @@ -43,25 +44,36 @@ def sigh(signum: int, _frame): shutdown(status) -def playback_start(): +async def mpv_start(args: list[str] = ["--idle"]): global mpv_process mpv_process = subprocess.Popen( [ "mpv", f"--input-ipc-server={mpv_socket}", - "--shuffle", - # "--loop-playlist", "--no-video", f"--volume={volume}", - str(music_path), + *args, ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) open(mpv_pidfile, "w").write(str(mpv_process.pid)) + while not mpv_socket.exists(): + await asyncio.sleep(0.01) -def playback_stop(): + +async def playback_start(args: list[str] = ["--idle"]): + return await mpv_start( + args=[ + "--shuffle", + # "--loop-playlist", + str(music_path), + ] + ) + + +def mpv_stop(): global mpv_process if mpv_process is not None: mpv_process.terminate() @@ -71,7 +83,7 @@ def playback_stop(): mpv_pidfile.unlink() -def is_playing(): +def mpv_running(): global mpv_process running = mpv_process is not None if mpv_process is not None: @@ -81,30 +93,33 @@ def is_playing(): return running -def mpv_socket_open() -> socket.socket | None: - if is_playing(): - sock = socket.socket(socket.AF_UNIX) +async def mpv_socket_open() -> tuple[StreamReader, StreamWriter] | None: + async def socket_open_helper(): while True: try: - sock.connect(str(mpv_socket).encode()) - break - except (ConnectionRefusedError, FileNotFoundError): + return await asyncio.open_unix_connection(mpv_socket) + except ConnectionRefusedError: pass - return sock + if mpv_running(): + try: + return await asyncio.wait_for(socket_open_helper(), timeout=3) + except asyncio.TimeoutError: + mpv_stop() return None -def mpv_socket_command( - sock: socket.socket, command: dict[str, Any] -) -> Mapping[str, Any]: - sock.send((json.dumps(command) + "\n").encode()) +async def mpv_socket_command( + reader: StreamReader, writer: StreamWriter, command: dict[str, Any] +) -> Mapping[str, Any] | None: + if writer.is_closing(): + return - reply = b"" - while (data := sock.recv(1)) != b"\n": - reply += data + writer.write((json.dumps(command) + "\n").encode()) + await writer.drain() + reply = await reader.readline() return json.loads(reply.decode()) @@ -118,20 +133,20 @@ def sizeof_fmt(num, suffix="B"): @app.route("/", methods=["GET"]) def route_interface(): - return render_template("player.html", playing=is_playing()) + return render_template("player.html", playing=mpv_running()) @app.route("/", methods=["POST"]) -def route_toggle(): +async def route_toggle(): global mpv_process - if is_playing(): + if mpv_running(): print("Stopping Audio Playback..") - playback_stop() + mpv_stop() else: print("Starting Audio Playback..") - playback_start() + await playback_start() - return render_template("player.html", playing=is_playing()) + return render_template("player.html", playing=mpv_running()) @app.route("/files/<path:path>", methods=["GET"]) @@ -156,33 +171,33 @@ def filemgr(path=""): @app.route("/api/start", methods=["POST"]) -def api_start(): - if is_playing(): +async def api_start(): + if mpv_running(): return jsonify("Laas is already lofi'ing"), 400 else: - playback_start() + await playback_start() return jsonify("ok"), 200 @app.route("/api/stop", methods=["POST"]) def api_stop(): - if not is_playing(): + if not mpv_running(): return jsonify("You cant stop when theres no playback, womp womp!"), 400 else: - playback_stop() + mpv_stop() return jsonify("ok"), 200 @app.route("/api/status", methods=["GET"]) def api_status(): - return jsonify(is_playing()) + return jsonify(mpv_running()) @app.route("/api/nowplaying", methods=["GET"]) -def api_nowplaying(): +async def api_nowplaying(): response: dict[str, Any] = {"status": "stopped"} - sock = mpv_socket_open() + sock = await mpv_socket_open() if sock is not None: response["status"] = "playing" @@ -199,16 +214,21 @@ def api_nowplaying(): else: key = prop - reply_json = mpv_socket_command(sock, {"command": ["get_property", prop]}) + reply_json = await mpv_socket_command( + *sock, {"command": ["get_property", prop]} + ) + if reply_json is None: + break response[key] = reply_json["data"] if "data" in reply_json else "Unknown" - sock.close() + sock[1].close() + await sock[1].wait_closed() return jsonify(response) @app.route("/api/play/<path:filename_or_url>", methods=["POST"]) -def api_play_file( +async def api_play_file( filename_or_url: str, error_str: str = "Could not play file '{filename}'" ): if re.match("^https?://.*", filename_or_url): @@ -219,23 +239,19 @@ def api_play_file( return jsonify(error_str.format(filename=filename_or_url)), 404 playback_uri = str(file_path) - if not is_playing(): - playback_start() + if mpv_running(): + mpv_stop() - sock = mpv_socket_open() - if sock is not None: - mpv_socket_command(sock, {"command": ["loadfile", playback_uri]}) - sock.close() + await mpv_start([playback_uri]) - time.sleep(0.1) - if is_playing(): - return jsonify("ok") + if not mpv_running(): + return jsonify(error_str.format(filename=filename_or_url)), 500 - return jsonify(error_str.format(filename=filename_or_url)), 500 + return jsonify("ok") @app.route("/api/play", methods=["POST"]) -def api_play_file2(error_str: str = "Could not play file '{filename}'"): +async def api_play_file2(error_str: str = "Could not play file '{filename}'"): filename_or_url = request.get_data().decode() if re.match("^https?://.*", filename_or_url): playback_uri = filename_or_url @@ -245,30 +261,26 @@ def api_play_file2(error_str: str = "Could not play file '{filename}'"): return jsonify(error_str.format(filename=filename_or_url)), 404 playback_uri = str(file_path) - if not is_playing(): - playback_start() + if mpv_running(): + mpv_stop() - sock = mpv_socket_open() - if sock is not None: - mpv_socket_command(sock, {"command": ["loadfile", playback_uri]}) - sock.close() + await mpv_start([playback_uri]) - time.sleep(0.1) - if is_playing(): - return jsonify("ok") + if not mpv_running(): + return jsonify(error_str.format(filename=filename_or_url)), 500 - return jsonify(error_str.format(filename=filename_or_url)), 500 + return jsonify("ok") @app.route("/api/isengard", methods=["POST"]) -def api_isengard(): - return api_play_file( +async def api_isengard(): + return await api_play_file( "isengard.mp3", error_str="Could not take the hobbits to Isengard" ) @app.route("/api/volume", methods=["GET", "PUT"]) -def api_volume(): +async def api_volume(): if request.method == "PUT": global volume try: @@ -278,10 +290,11 @@ def api_volume(): except Exception: return jsonify("bad volume"), 400 - sock = mpv_socket_open() + sock = await mpv_socket_open() if sock is not None: - mpv_socket_command(sock, {"command": ["set", "volume", str(volume)]}) - sock.close() + await mpv_socket_command(*sock, {"command": ["set", "volume", str(volume)]}) + sock[1].close() + await sock[1].wait_closed() return jsonify(volume) @@ -297,4 +310,4 @@ if __name__ == "__main__": try: app.run(host="::", port=1337) finally: - playback_stop() + mpv_stop() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fafa8e7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +flask[async]