Compare commits

..

2 Commits
master ... tui

Author SHA1 Message Date
d716b5628a WIP: tui 2024-05-18 23:29:11 +02:00
76aaaa0a45 poetry: add textual, lock 2024-05-18 23:28:55 +02:00
9 changed files with 1044 additions and 475 deletions

View File

@ -13,7 +13,7 @@ A set of simple API endpoints and ready-to-go clients to interface with the [Grz
## How to run this
pip install --user git+https://git.pvv.ntnu.no/Grzegorz/grzegorz-clients.git#master
pip install --user git+https://github.com/Programvareverkstedet/grzegorz_clients.git#master
### cli
@ -23,7 +23,7 @@ A set of simple API endpoints and ready-to-go clients to interface with the [Grz
As the user intended to run the server:
pip install --user git+https://git.pvv.ntnu.no/Grzegorz/grzegorz-clients.git#master
pip install --user git+https://github.com/Programvareverkstedet/grzegorz_clients.git#master
grzegorz-webui --host-name 0.0.0.0 --port 80
It's rather insecure and could use a reverse proxy and some whitelisting. ;)

6
flake.lock generated
View File

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1726755586,
"narHash": "sha256-PmUr/2GQGvFTIJ6/Tvsins7Q43KTMvMFhvG6oaYK+Wk=",
"lastModified": 1711703276,
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c04d5652cfa9742b1d519688f65d1bbccea9eb7e",
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
"type": "github"
},
"original": {

View File

@ -57,28 +57,14 @@
nativeBuildInputs = [ poetry-core ];
propagatedBuildInputs = [ setuptools flakes.self.pkgs.remi requests typer rich urllib3 ];
};
grzegorzctl = pkgs.runCommandNoCCLocal "grzegorzctl" (
{
nativeBuildInputs = [ pkgs.installShellFiles ];
} //
{ inherit (flakes.self.pkgs.grzegorz-clients) meta; } //
{ meta.mainProgram = "grzegorzctl"; }
)''
mkdir -p $out/bin
ln -s "${flakes.self.pkgs.grzegorz-clients}/bin/grzegorzctl" $out/bin/grzegorzctl
installShellCompletion --cmd grzegorzctl \
--bash <($out/bin/grzegorzctl --show-completion bash) \
--zsh <($out/bin/grzegorzctl --show-completion zsh) \
--fish <($out/bin/grzegorzctl --show-completion fish)
'';
default = flakes.self.pkgs.grzegorzctl;
default = flakes.self.pkgs.grzegorz-clients;
});
apps = forAllSystems ({ system, ...}: rec {
grzegorz-webui.type = "app";
grzegorz-webui.program = "${self.packages.${system}.grzegorz-clients}/bin/grzegorz-webui";
grzegorzctl.type = "app";
grzegorzctl.program = "${self.packages.${system}.grzegorzctl}/bin/grzegorzctl";
grzegorzctl.program = "${self.packages.${system}.grzegorz-clients}/bin/grzegorzctl";
default = grzegorzctl;
});

View File

@ -64,7 +64,7 @@ def _add(
api.playlist_goto(current_index if put_pre else current_index + 1)
api.set_playing(True)
@cli.command(help="Add one or more items to the playlist")
@cli.command(help="Add one ore more items to the playlist")
def play(
urls: list[str],
pre: bool = False,
@ -72,14 +72,14 @@ def play(
):
_add(urls, put_post=not pre, put_pre=pre, play=True, api_base=api_base)
@cli.command(help="Add one or more items to the playlist")
@cli.command(help="Add one ore more items to the playlist")
def next(
urls: list[str],
api_base: str = DEFAULT_API_BASE,
):
_add(urls, put_post=True, api_base=api_base)
@cli.command(help="Add one or more items to the playlist")
@cli.command(help="Add one ore more items to the playlist")
def queue(
urls: list[str],
play: bool = True,
@ -107,12 +107,6 @@ 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="Toggle playback")
def toggle(api_base: str = DEFAULT_API_BASE):
api.set_endpoint(api_base)
playing = api.is_playing()
rich.print(api.set_playing(not playing), file=sys.stderr)
@cli.command(help="Goto next item in playlist")
def skip( api_base: str = DEFAULT_API_BASE ):
api.set_endpoint(api_base)
@ -160,21 +154,11 @@ def status(
@cli.command(help="Set the playback volume")
def set_volume(
volume: str,
volume: int,
api_base: str = DEFAULT_API_BASE,
):
api.set_endpoint(api_base)
volume = volume.removesuffix("%")
if volume.startswith("+") or volume.startswith("-"):
current_volume = api.get_volume()
new_volume = max(0, min(100, current_volume + int(volume)))
new_volume = int(new_volume)
else:
new_volume = int(volume)
rich.print(api.set_volume(new_volume), file=sys.stderr)
rich.print(api.set_volume(volume), file=sys.stderr)
if __name__ == "__main__":

View File

@ -0,0 +1,23 @@
/* https://textual.textualize.io/guide/design/#theme-reference */
Screen {
align: center top;
box-sizing: border-box;
}
Horizontal {
height: auto;
}
#header {
height: 1;
overflow: hidden;
}
#controls {
background: $background-lighten-2;
align: center middle;
}
#volumebar {
}
#seekbar {
}
DataTable#playlist {
width: 100%;
}

View File

@ -294,7 +294,7 @@ class RemiApp(App):
except api.APIError:
playback_pos = None
if playback_pos and isinstance(playback_pos, dict) and playback_pos["current"] and playback_pos["total"]:
if playback_pos:
slider_pos = playback_pos["current"] / playback_pos["total"] * 100
current = seconds_to_timestamp(playback_pos["current"])
total = seconds_to_timestamp(playback_pos["total"])

172
grzegorz_clients/tui.py Normal file
View File

@ -0,0 +1,172 @@
from . import api, utils
from pathlib import Path
from datetime import timedelta
import json
import os
import rich
import shutil
import subprocess
import sys
import typer
from textual.app import App, ComposeResult
from textual.containers import Vertical, Horizontal, VerticalScroll
from textual.widgets import Button, Static, ProgressBar, DataTable
from textual.timer import Timer
from textual import on
from textual.app import App, ComposeResult
import textual.containers as c
from textual.reactive import var
import textual.widgets as w
from textual.events import Mount
from textual.widgets import DirectoryTree, Footer, Header, Static
# export GRZEGORZ_DEFAULT_API_BASE="https://brzeczyszczykiewicz.pvv.ntnu.no/api"
# export GRZEGORZ_DEFAULT_API_BASE="https://georg.pvv.ntnu.no/api"
DEFAULT_API_BASE = os.environ.get("GRZEGORZ_DEFAULT_API_BASE", "https://brzeczyszczykiewicz.pvv.ntnu.no/api")
# url input
# prev - Button
# play/pause - Button
# next - Button
# loop - Switch
# shuffle - Button
# clear - Button
# position - ProgressBar
# playlist - datatable
# waveform - Sparkline
# LoadingIndicator
class GrzegorzApp(App):
CSS_PATH = "gzregorz.tcss"
#DEFAULT_CSS = ""
BINDINGS = [
("q", "quit", "Quit"),
]
refresh_timer: Timer
def compose(self) -> ComposeResult:
yield Static("Now playing: ", id="header")
yield Horizontal(
ProgressBar(id="seekbar", total=0, show_percentage=False, show_eta=False),
Static(" --:-- --:--", id="playtime"),
classes="center"
)
yield Horizontal(
ProgressBar(id="volumebar", total=100, show_eta=False),
classes="center"
)
yield Horizontal(
Button.error("Clear", id="btn-clear"),
Button("Prev", id="btn-prev"),
Button.success("Play", id="btn-play"),
Button("Next", variant="primary", id="btn-next"),
Button.warning("Shuffle", id="btn-shuffle"),
id="controls",
)
yield VerticalScroll(
DataTable(
id="playlist",
zebra_stripes=True,
cursor_type="row",
),
)
def on_mount(self) -> None:
api.set_endpoint("https://georg.pvv.ntnu.no/api")
self.refresh_timer = self.set_interval(1 / 1, self.do_update) # threaded
# self.log(api.get_playlist())
playlist: DataTable = self.query_one("#playlist")
playlist.add_columns("#", "Name", "length")
def do_update(self):
self.log("do_update")
# update status
vol = api.get_volume()
pos = api.get_playback_pos() or {"current": 0, "left": 0, "total": 0}
loop = api.get_playlist_looping()
play = api.is_playing()
self.log(f"""
{vol = }
{pos = }
{loop = }
{play = }
""")
# todo: sliders?
seek_var: ProgressBar = self.query_one("#seekbar")
seek_var.total = pos.get("total", 0)
seek_var.progress = pos.get("current", 0)
playtime: Static = self.query_one("#playtime")
playtime.update(f" --:-- - --:--" if not play else f" {timedelta(seconds=int(pos.get('current', 0)))} - {timedelta(seconds=int(pos.get('total', 0)))}")
vol_var: ProgressBar = self.query_one("#volumebar")
vol_var.progress = vol
btn_play: Button = self.query_one("#btn-play")
btn_play.label = "Pause" if play else "Play"
# btn_play.variant = "success" if play else "default"
# update playlist
playlist_data = api.get_playlist()
table = [
(
item.get("index", -1),
item.get("data", {}).get("title", None) or item["filename"],
utils.seconds_to_timestamp(item["data"]["duration"]) if "duration" in item.get("data", {}) else "--:--",
)
for item in playlist_data
]
current, = [item for item in playlist_data if item.get("current", False)][:1] or [None]
if current is not None:
self.query_one("#header").update("Now playing: " +
current.get("data", {}).get("title", None) or current["filename"]
)
playlist: DataTable = self.query_one("#playlist")
for r, row in enumerate(table[:playlist.row_count]):
for c, col in enumerate(row):
playlist.update_cell_at((r, c), col)
if playlist.row_count < len(table): # add more rows
playlist.add_rows(table[playlist.row_count:])
elif playlist.row_count > len(table): # remove extra rows
for i in range(playlist.row_count-1, len(table)-1, -1):
playlist.remove_row(playlist._row_locations.get_key(i))
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "btn-clear":
api.playlist_clear()
elif event.button.id == "btn-prev":
api.playlist_previous()
elif event.button.id == "btn-play":
api.set_playing(not api.is_playing())
elif event.button.id == "btn-next":
api.playlist_next()
elif event.button.id == "btn-shuffle":
api.playlist_shuffle()
@on(DataTable.RowSelected)
def on_data_table_row_selected(self, event: DataTable.RowSelected):
self.log("spismeg")
self.log(f"{event = }")
self.log(f"{event.cursor_row = }")
self.log(f"{event.row_key = }")
self.log("spismeg")
def main():
app = GrzegorzApp()
app.run()
if __name__ == "__main__":
main()

1259
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,19 +6,22 @@ authors = ["Peder Bergebakken Sundt <pbsds@hotmail.com>"]
license = "MIT"
[tool.poetry.dependencies]
python = ">=3.7,<4.0"
python = ">=3.8.1,<4.0"
remi = "1.0"
requests = ">=2.27.1,<3"
rich = ">=13.3.5"
typer = ">=0.4.0"
urllib3 = ">=1.26.8,<3"
textual = {version = "^0.61.0", extras = ["tui"]}
[tool.poetry.dev-dependencies]
python-lsp-server = {extras = ["all"], version = "^1.5.0"}
[tool.poetry.group.dev.dependencies]
python-lsp-server = {extras = ["all"], version = "^1.11.0"}
textual-dev = "^1.5.1"
[tool.poetry.scripts]
grzegorz-webui = "grzegorz_clients.__main__:cli"
grzegorzctl = "grzegorz_clients.cli:cli"
grzegorztui = "grzegorz_clients.tui:main"
[build-system]
requires = ["poetry-core>=1.0.0"]