From b23b02b5e82b36b536f181f5a07ba1791ce1f573 Mon Sep 17 00:00:00 2001 From: Peder Bergebakken Sundt Date: Sun, 31 Mar 2024 04:46:54 +0200 Subject: [PATCH] grzegorzctl --- README.md | 10 ++- flake.nix | 6 +- grzegorz_clients/api.py | 3 +- grzegorz_clients/cli.py | 140 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 4 +- 5 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 grzegorz_clients/cli.py diff --git a/README.md b/README.md index 99fef8b..f9d315c 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,22 @@ A set of simple API endpoints and ready-to-go clients to interface with the [Grz #### Working clients: * A webUI client made with REMI +* CLI client #### Planned future clients: -* CLI client * WebExtensions browser extension ## How to run this + pip install --user git+https://github.com/Programvareverkstedet/grzegorz_clients.git#master + +### cli + + grzegorzctl + +### webui + As the user intended to run the server: pip install --user git+https://github.com/Programvareverkstedet/grzegorz_clients.git#master diff --git a/flake.nix b/flake.nix index 2a612a3..2ae6bcb 100644 --- a/flake.nix +++ b/flake.nix @@ -55,7 +55,7 @@ format = "pyproject"; src = ./.; nativeBuildInputs = [ poetry-core ]; - propagatedBuildInputs = [ setuptools flakes.self.pkgs.remi requests typer urllib3 ]; + propagatedBuildInputs = [ setuptools flakes.self.pkgs.remi requests typer rich urllib3 ]; }; default = flakes.self.pkgs.grzegorz-clients; }); @@ -63,7 +63,9 @@ apps = forAllSystems ({ system, ...}: rec { grzegorz-webui.type = "app"; grzegorz-webui.program = "${self.packages.${system}.grzegorz-clients}/bin/grzegorz-webui"; - default = grzegorz-webui; + grzegorzctl.type = "app"; + grzegorzctl.program = "${self.packages.${system}.grzegorz-clients}/bin/grzegorzctl"; + default = grzegorzctl; }); nixosModules.grzegorz-webui = { config, pkgs, ... }: let diff --git a/grzegorz_clients/api.py b/grzegorz_clients/api.py index acfbe0b..9202956 100644 --- a/grzegorz_clients/api.py +++ b/grzegorz_clients/api.py @@ -69,7 +69,7 @@ def get_volume(): return "volume" @request_post -def set_volume(volume: int): # between 0 and 100 (you may also exceed 100) +def set_volume(volume: float): # between 0 and 100 (you may also exceed 100) args = urlencode(locals()) return f"volume?{args}", None @@ -116,7 +116,6 @@ def get_playlist_looping(): def playlist_set_looping(looping: bool): return f"playlist/loop?loop={str(bool(looping)).lower()}", None - @request_get def get_playback_pos(): return "time" diff --git a/grzegorz_clients/cli.py b/grzegorz_clients/cli.py new file mode 100644 index 0000000..1fe332f --- /dev/null +++ b/grzegorz_clients/cli.py @@ -0,0 +1,140 @@ +from . import api +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +import json +import os +import rich +import shutil +import subprocess +import sys +import typer + +# export GRZEGORZ_DEFAULT_API_BASE="https://georg.pvv.ntnu.no/api" +# export GRZEGORZ_DEFAULT_API_BASE="https://brzeczyszczykiewicz.pvv.ntnu.no/api" +# export GRZEGORZ_DEFAULT_API_BASE="https://georg.pvv.ntnu.no/api;https://brzeczyszczykiewicz.pvv.ntnu.no/api" + +DEFAULT_API_BASE = os.environ.get("GRZEGORZ_DEFAULT_API_BASE", "https://brzeczyszczykiewicz.pvv.ntnu.no/api") +if ";" in DEFAULT_API_BASE: + if shutil.which("gum"): + DEFAULT_API_BASE = subprocess.run([ + "gum", "choose", + *DEFAULT_API_BASE.split(";"), + "--header=Please select an endpoint...", + ], check=True, text=True, stdout=subprocess.PIPE).stdout.strip() + else: + DEFAULT_API_BASE = DEFAULT_API_BASE.split(";", 1)[0] + +def print_json(obj): + if not isinstance(obj, str): + obj = json.dumps(obj) + rich.print_json(obj) + +cli = typer.Typer(no_args_is_help=True) + + +@cli.command(help="Add one ore more items to the playlist. [--play, --now]") +def add( + urls: list[str], + play: bool = False, + now: bool = False, + api_base: str = DEFAULT_API_BASE, +): + api.set_endpoint(api_base) + if now: + pre = api.get_playlist() + + for url in urls: + resp = api.load_path(url) + rich.print(f"{url} : {resp!r}", file=sys.stderr) + + if now: + post = api.get_playlist() + current_index, = [i.get("index", -1) for i in post if i.get("current", False)][:1] or [0] + old_indices = set(i.get("index", -1) for i in pre) + new_indices = set(i.get("index", -1) for i in post) - old_indices + assert all(i > current_index for i in new_indices) + target = current_index + if not play: target += 1 + if target not in new_indices: + for idx in sorted(new_indices): + api.playlist_move(idx, target) + target += 1 + + if play: + if now: api.playlist_goto(current_index) + api.set_playing(True) + +@cli.command(name="list", help="List the current playlist") +def list_( + api_base: str = DEFAULT_API_BASE, +): + api.set_endpoint(api_base) + print_json(api.get_playlist()) + +@cli.command(help="Set Playing") +def play( api_base: str = DEFAULT_API_BASE ): + api.set_endpoint(api_base) + # TODO: add logic to seek to start of song if at end of song AND at end of playlist? + rich.print(api.set_playing(True), file=sys.stderr) + +@cli.command(help="Unset Playing") +def pause( api_base: str = DEFAULT_API_BASE ): + api.set_endpoint(api_base) + rich.print(api.set_playing(False), file=sys.stderr) + +@cli.command(help="Goto next item in playlist") +def next( api_base: str = DEFAULT_API_BASE ): + api.set_endpoint(api_base) + rich.print(api.playlist_next(), file=sys.stderr) + +@cli.command(help="Goto previous item in playlist") +def prev( api_base: str = DEFAULT_API_BASE ): + api.set_endpoint(api_base) + rich.print(api.playlist_previous(), file=sys.stderr) + +@cli.command(help="Goto a specific item in the playlist") +def goto( index: int, api_base: str = DEFAULT_API_BASE ): + api.set_endpoint(api_base) + rich.print(api.playlist_goto(index), file=sys.stderr) + +@cli.command(help="Shuffle the playlist") +def shuf( api_base: str = DEFAULT_API_BASE ): + api.set_endpoint(api_base) + rich.print(api.playlist_shuffle(), file=sys.stderr) + +@cli.command(help="Clear the playlist") +def clear( api_base: str = DEFAULT_API_BASE ): + api.set_endpoint(api_base) + rich.print(api.playlist_clear(), file=sys.stderr) + +@cli.command(help="Get current status") +def status( + api_base: str = DEFAULT_API_BASE, +): + api.set_endpoint(api_base) + + status = dict( + playing = api.is_playing, + volume = api.get_volume, + is_looping = api.get_playlist_looping, + playback_pos = api.get_playback_pos, + current = lambda: [i for i in api.get_playlist() if i.get("current", False)][:1] or [None], + ) + + with ThreadPoolExecutor() as p: + status = dict(zip(status.keys(), p.map(lambda x: x(), status.values()))) + + print_json(status) + + +@cli.command(help="Set the playback volume") +def set_volume( + volume: float, + api_base: str = DEFAULT_API_BASE, +): + api.set_endpoint(api_base) + rich.print(api.set_volume(volume), file=sys.stderr) + + +if __name__ == "__main__": + cli() diff --git a/pyproject.toml b/pyproject.toml index ea62e37..62920db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,14 +9,16 @@ license = "MIT" python = ">=3.7,<4.0" remi = "1.0" requests = ">=2.27.1,<3" +rich = ">=13.3.5" typer = ">=0.4.0" -urllib3 = "^1.26.8" +urllib3 = ">=1.26.8,<3" [tool.poetry.dev-dependencies] python-lsp-server = {extras = ["all"], version = "^1.5.0"} [tool.poetry.scripts] grzegorz-webui = "grzegorz_clients.__main__:cli" +grzegorzctl = "grzegorz_clients.cli:cli" [build-system] requires = ["poetry-core>=1.0.0"]