diff --git a/.gitignore b/.gitignore index 7fe82f0..c385ae3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc __pycache__ config.py +*.socket diff --git a/grzegorz/api.py b/grzegorz/api.py index 8091331..9f5a070 100644 --- a/grzegorz/api.py +++ b/grzegorz/api.py @@ -1,8 +1,7 @@ -import asyncio -from sanic import Blueprint, response +from sanic import Request, Blueprint, response from sanic_openapi import doc from functools import wraps -from . import mpv +from .mpv import MPVControl from .playlist_data import PlaylistDataCache bp = Blueprint("grzegorz-api", strict_slashes=True) @@ -13,7 +12,7 @@ bp = Blueprint("grzegorz-api", strict_slashes=True) def response_json(func): @wraps(func) async def newfunc(*args, **kwargs): - try: + try: request = args[0] mpv_control = request.app.config["mpv_control"] out = await func(*args, mpv_control, **kwargs) @@ -37,15 +36,17 @@ def response_text(func): return response.text(body) return newfunc -class APIError(Exception): pass +class APIError(Exception): + pass +# singleton PLAYLIST_DATA_CACHE = PlaylistDataCache(auto_fetch_data=True) #routes: @bp.get("") @doc.exclude(True) @response_text -async def root(request): +async def root(request: Request): return "Hello friend, I hope you're having a lovely day" @bp.post("/load") @@ -53,7 +54,7 @@ async def root(request): @doc.consumes({"path": doc.String("Link to the resource to enqueue")}, required=True) @doc.consumes({"body":doc.Dictionary(description="Any data you want stored with the queued item")}, location="body") @response_json -async def loadfile(request, mpv_control): +async def loadfile(request: Request, mpv_control: MPVControl): if "path" not in request.args: raise APIError("No query parameter \"path\" provided") if request.json: @@ -64,7 +65,7 @@ async def loadfile(request, mpv_control): @bp.get("/play") @doc.summary("Check whether the player is paused or playing") @response_json -async def play_get(request, mpv_control): +async def play_get(request: Request, mpv_control: MPVControl): value = await mpv_control.pause_get() == False return locals() @@ -72,7 +73,7 @@ async def play_get(request, mpv_control): @doc.summary("Set whether the player is paused or playing") @doc.consumes({"play": doc.Boolean("Whether to be playing or not")}) @response_json -async def play_set(request, mpv_control): +async def play_set(request: Request, mpv_control: MPVControl): if "play" not in request.args: raise APIError("No query parameter \"play\" provided") success = await mpv_control \ @@ -82,7 +83,7 @@ async def play_set(request, mpv_control): @bp.get("/volume") @doc.summary("Get the current player volume") @response_json -async def volume_get(request, mpv_control): +async def volume_get(request: Request, mpv_control: MPVControl): value = await mpv_control.volume_get() return locals() @@ -90,7 +91,7 @@ async def volume_get(request, mpv_control): @doc.summary("Set the player volume") @doc.consumes({"volume": doc.Integer("A number between 0 and 100")}) @response_json -async def volume_set(request, mpv_control): +async def volume_set(request: Request, mpv_control: MPVControl): if "volume" not in request.args: raise APIError("No query parameter \"volume\" provided") success = await mpv_control \ @@ -100,7 +101,7 @@ async def volume_set(request, mpv_control): @bp.get("/time") @doc.summary("Get current playback position") @response_json -async def time_get(request, mpv_control): +async def time_get(request: Request, mpv_control: MPVControl): value = { "current": await mpv_control.time_pos_get(), "left": await mpv_control.time_remaining_get(), @@ -112,7 +113,7 @@ async def time_get(request, mpv_control): @doc.summary("Set playback position") @doc.consumes({"pos": doc.Float("Seconds to seek to"), "pos": doc.Integer("Percent to seek to")}) @response_json -async def time_set(request, mpv_control): +async def time_set(request: Request, mpv_control: MPVControl): if "pos" in request.args: success = await mpv_control.seek_absolute(float(request.args["pos"][0])) elif "percent" in request.args: @@ -124,7 +125,7 @@ async def time_set(request, mpv_control): @bp.get("/playlist") @doc.summary("Get the current playlist") @response_json -async def playlist_get(request, mpv_control): +async def playlist_get(request: Request, mpv_control: MPVControl): value = await mpv_control.playlist_get() value = list(PLAYLIST_DATA_CACHE.add_data_to_playlist(value)) for i, v in enumerate(value): @@ -137,14 +138,14 @@ async def playlist_get(request, mpv_control): @bp.post("/playlist/next") @doc.summary("Skip to the next item in the playlist") @response_json -async def playlist_next(request, mpv_control): +async def playlist_next(request: Request, mpv_control: MPVControl): success = await mpv_control.playlist_next() return locals() @bp.post("/playlist/previous") @doc.summary("Go back to the previous item in the playlist") @response_json -async def playlist_previous(request, mpv_control): +async def playlist_previous(request: Request, mpv_control: MPVControl): success = await mpv_control.playlist_prev() return locals() @@ -152,7 +153,7 @@ async def playlist_previous(request, mpv_control): @doc.summary("Go chosen item in the playlist") @doc.consumes({"index": doc.Integer("The 0 indexed playlist item to go to")}, required=True) @response_json -async def playlist_goto(request, mpv_control): +async def playlist_goto(request: Request, mpv_control: MPVControl): if "index" not in request.args: raise APIError("Missing the required parameter: \"index\"") success = await mpv_control.playlist_goto( @@ -163,7 +164,7 @@ async def playlist_goto(request, mpv_control): @doc.summary("Clears single item or whole playlist") @doc.consumes({"index": doc.Integer("Index to item in playlist to remove. If unset, the whole playlist is cleared")}) @response_json -async def playlist_remove_or_clear(request, mpv_control): +async def playlist_remove_or_clear(request: Request, mpv_control: MPVControl): if "index" in request.args: success = await mpv_control.playlist_remove(int(request.args["index"][0])) action = f"remove #{request.args['index'][0]}" @@ -183,7 +184,7 @@ async def playlist_remove_or_clear(request, mpv_control): @doc.consumes({"index2": int}, required=True) @doc.consumes({"index1": int}, required=True) @response_json -async def playlist_move(request, mpv_control): +async def playlist_move(request: Request, mpv_control: MPVControl): if "index1" not in request.args or "index2" not in request.args: raise APIError( "Missing at least one of the required query " @@ -196,14 +197,14 @@ async def playlist_move(request, mpv_control): @bp.post("/playlist/shuffle") @doc.summary("Clears single item or whole playlist") @response_json -async def playlist_shuffle(request, mpv_control): +async def playlist_shuffle(request: Request, mpv_control: MPVControl): success = await mpv_control.playlist_shuffle() return locals() @bp.get("/playlist/loop") @doc.summary("See whether it loops the playlist or not") @response_json -async def playlist_get_looping(request, mpv_control): +async def playlist_get_looping(request: Request, mpv_control: MPVControl): value = await mpv_control.playlist_get_looping() return locals() @@ -211,7 +212,7 @@ async def playlist_get_looping(request, mpv_control): @doc.summary("Sets whether to loop the playlist or not") @doc.consumes({"loop": doc.Boolean("Whether to be looping or not")}, required=True) @response_json -async def playlist_set_looping(request, mpv_control): +async def playlist_set_looping(request: Request, mpv_control: MPVControl): if "loop" not in request.args: raise APIError("Missing the required parameter: \"loop\"") success = await mpv_control.playlist_set_looping( diff --git a/grzegorz/mpv.py b/grzegorz/mpv.py index 318cbd4..763d51a 100644 --- a/grzegorz/mpv.py +++ b/grzegorz/mpv.py @@ -2,9 +2,11 @@ import os import asyncio import json from shlex import quote +from typing import List, Optional, Union from . import nyasync + class MPV: _ipc_endpoint = 'mpv_ipc.socket' @@ -69,7 +71,8 @@ class MPV: else: # response self.responses.put_nowait(msg) -class MPVError(Exception): pass +class MPVError(Exception): + pass class MPVControl: def __init__(self): @@ -84,6 +87,7 @@ class MPVControl: async def process_events(self): async for event in self.mpv.events: + # TODO: print? pass async def send_request(self, msg): @@ -93,9 +97,11 @@ class MPVControl: # is the safest option. self.mpv.requests.put_nowait(msg) return await self.mpv.responses.get() - + #other commands: async def wake_screen(self): + # TODO: use this + # TODO: wayland counterpart p = await asyncio.create_subprocess_exec( "xset", "-display", @@ -105,75 +111,99 @@ class MPVControl: "on" ) code = await process.wait() - + #Shorthand command requests: - async def loadfile(self, file):#appends to playlist and start playback if paused + + async def loadfile(self, file: Union[str, Path]): + "appends to playlist and start playback if paused" + + if isinstance(file, Path): + file = str(file) resp = await self.send_request({"command":["loadfile", file, "append-play"]}) return resp["error"] == "success" + async def pause_get(self): resp = await self.send_request({"command":["get_property", "pause"]}) if "error" in resp and resp["error"] != "success": raise MPVError("Unable to get whether paused or not: " + resp["error"]) return resp["data"] if "data" in resp else None - async def pause_set(self, state): + + async def pause_set(self, state: bool): resp = await self.send_request({"command":["set_property", "pause", bool(state)]}) return resp["error"] == "success" + async def volume_get(self): resp = await self.send_request({"command":["get_property", "volume"]}) if "error" in resp and resp["error"] != "success": raise MPVError("Unable to get volume! " + resp["error"]) return resp["data"] if "data" in resp else None - async def volume_set(self, volume): + + async def volume_set(self, volume: int): resp = await self.send_request({"command":["set_property", "volume", volume]}) return resp["error"] == "success" + async def time_pos_get(self): resp = await self.send_request({"command":["get_property", "time-pos"]}) if "error" in resp and resp["error"] != "success": raise MPVError("Unable to get time pos: " + resp["error"]) return resp["data"] if "data" in resp else None + async def time_remaining_get(self): resp = await self.send_request({"command":["get_property", "time-remaining"]}) if "error" in resp and resp["error"] != "success": raise MPVError("Unable to get time left:" + resp["error"]) return resp["data"] if "data" in resp else None - async def seek_absolute(self, seconds): + + async def seek_absolute(self, seconds: float): resp = await self.send_request({"command":["seek", seconds, "absolute"]}) return resp["data"] if "data" in resp else None - async def seek_relative(self, seconds): + + async def seek_relative(self, seconds: float): resp = await self.send_request({"command":["seek", seconds, "relative"]}) return resp["data"] if "data" in resp else None - async def seek_percent(self, percent): + + async def seek_percent(self, percent: float): resp = await self.send_request({"command":["seek", percent, "absolute-percent"]}) return resp["data"] if "data" in resp else None + async def playlist_get(self): resp = await self.send_request({"command":["get_property", "playlist"]}) if "error" in resp and resp["error"] != "success": raise MPVError("Unable to get playlist:" + resp["error"]) return resp["data"] if "data" in resp else None + async def playlist_next(self): resp = await self.send_request({"command":["playlist-next", "weak"]}) return resp["error"] == "success" + async def playlist_prev(self): resp = await self.send_request({"command":["playlist-prev", "weak"]}) return resp["error"] == "success" + async def playlist_goto(self, index): resp = await self.send_request({"command":["set_property", "playlist-pos", index]}) return resp["error"] == "success" + async def playlist_clear(self): resp = await self.send_request({"command":["playlist-clear"]}) return resp["error"] == "success" - async def playlist_remove(self, index=None): + + async def playlist_remove(self, index: Optional[int] = None): resp = await self.send_request({"command":["playlist-remove", "current" if index==None else index]}) return resp["error"] == "success" - async def playlist_move(self, index1, index2): + + async def playlist_move(self, index1: int, index2: int): resp = await self.send_request({"command":["playlist-move", index1, index2]}) return resp["error"] == "success" + async def playlist_shuffle(self): resp = await self.send_request({"command":["playlist-shuffle"]}) return resp["error"] == "success" + async def playlist_get_looping(self): resp = await self.send_request({"command":["get_property", "loop-playlist"]}) return resp["data"] == "inf" if "data" in resp else False - async def playlist_set_looping(self, value): + + async def playlist_set_looping(self, value: bool): resp = await self.send_request({"command":["set_property", "loop-playlist", "inf" if value else "no"]}) return resp["error"] == "success" diff --git a/grzegorz/nyasync.py b/grzegorz/nyasync.py index 797cd55..fd684e3 100644 --- a/grzegorz/nyasync.py +++ b/grzegorz/nyasync.py @@ -1,4 +1,5 @@ import asyncio +from asyncio.streams import StreamReader, StreamWriter def ify(func): """Decorate func to run async in default executor""" @@ -57,9 +58,9 @@ async def unix_connection(path): return UnixConnection(*endpoints) class UnixConnection: - def __init__(self, reader, writer): - self.reader = reader - self.writer = writer + def __init__(self, reader: StreamReader, writer: StreamWriter): + self.reader: StreamReader = reader + self.writer: StreamWriter = writer def __aiter__(self): return self.reader.__aiter__() diff --git a/grzegorz/playlist_data.py b/grzegorz/playlist_data.py index 0a1db67..3a6a75c 100644 --- a/grzegorz/playlist_data.py +++ b/grzegorz/playlist_data.py @@ -1,4 +1,3 @@ -import asyncio from .metadatafetch import get_metadata from . import nyasync @@ -8,12 +7,14 @@ class PlaylistDataCache: self.filepath_data_map = {} self.auto_fetch_data = auto_fetch_data self.jobs = None + def add_data(self, filepath, data=None): if data: self.filepath_data_map[filepath] = data + async def run(self): if not self.auto_fetch_data: return - + self.jobs = nyasync.Queue() async for filename in self.jobs: print("Fetching metadata for ", repr(filename)) @@ -22,9 +23,10 @@ class PlaylistDataCache: if filename in self.filepath_data_map: self.filepath_data_map[filename].update(data) del self.filepath_data_map[filename]["fetching"] + def add_data_to_playlist(self, playlist): seen = set() - + for item in playlist: if "filename" in item: seen.add(item["filename"]) @@ -41,8 +43,7 @@ class PlaylistDataCache: yield new_item continue yield item - + not_seen = set(self.filepath_data_map.keys()) - seen for name in not_seen: del self.filepath_data_map[name] -