grzegorzctl

This commit is contained in:
Peder Bergebakken Sundt 2024-03-31 04:46:54 +02:00
parent 99f41e54c4
commit b23b02b5e8
5 changed files with 157 additions and 6 deletions

View File

@ -5,14 +5,22 @@ A set of simple API endpoints and ready-to-go clients to interface with the [Grz
#### Working clients: #### Working clients:
* A webUI client made with REMI * A webUI client made with REMI
* CLI client
#### Planned future clients: #### Planned future clients:
* CLI client
* WebExtensions browser extension * WebExtensions browser extension
## How to run this ## 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: As the user intended to run the server:
pip install --user git+https://github.com/Programvareverkstedet/grzegorz_clients.git#master pip install --user git+https://github.com/Programvareverkstedet/grzegorz_clients.git#master

View File

@ -55,7 +55,7 @@
format = "pyproject"; format = "pyproject";
src = ./.; src = ./.;
nativeBuildInputs = [ poetry-core ]; 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; default = flakes.self.pkgs.grzegorz-clients;
}); });
@ -63,7 +63,9 @@
apps = forAllSystems ({ system, ...}: rec { apps = forAllSystems ({ system, ...}: rec {
grzegorz-webui.type = "app"; grzegorz-webui.type = "app";
grzegorz-webui.program = "${self.packages.${system}.grzegorz-clients}/bin/grzegorz-webui"; 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 nixosModules.grzegorz-webui = { config, pkgs, ... }: let

View File

@ -69,7 +69,7 @@ def get_volume():
return "volume" return "volume"
@request_post @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()) args = urlencode(locals())
return f"volume?{args}", None return f"volume?{args}", None
@ -116,7 +116,6 @@ def get_playlist_looping():
def playlist_set_looping(looping: bool): def playlist_set_looping(looping: bool):
return f"playlist/loop?loop={str(bool(looping)).lower()}", None return f"playlist/loop?loop={str(bool(looping)).lower()}", None
@request_get @request_get
def get_playback_pos(): def get_playback_pos():
return "time" return "time"

140
grzegorz_clients/cli.py Normal file
View File

@ -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()

View File

@ -9,14 +9,16 @@ license = "MIT"
python = ">=3.7,<4.0" python = ">=3.7,<4.0"
remi = "1.0" remi = "1.0"
requests = ">=2.27.1,<3" requests = ">=2.27.1,<3"
rich = ">=13.3.5"
typer = ">=0.4.0" typer = ">=0.4.0"
urllib3 = "^1.26.8" urllib3 = ">=1.26.8,<3"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
python-lsp-server = {extras = ["all"], version = "^1.5.0"} python-lsp-server = {extras = ["all"], version = "^1.5.0"}
[tool.poetry.scripts] [tool.poetry.scripts]
grzegorz-webui = "grzegorz_clients.__main__:cli" grzegorz-webui = "grzegorz_clients.__main__:cli"
grzegorzctl = "grzegorz_clients.cli:cli"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]