Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
d716b5628a | |||
76aaaa0a45 |
@ -13,7 +13,7 @@ A set of simple API endpoints and ready-to-go clients to interface with the [Grz
|
|||||||
|
|
||||||
## How to run this
|
## 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
|
### 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:
|
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
|
grzegorz-webui --host-name 0.0.0.0 --port 80
|
||||||
|
|
||||||
It's rather insecure and could use a reverse proxy and some whitelisting. ;)
|
It's rather insecure and could use a reverse proxy and some whitelisting. ;)
|
||||||
|
6
flake.lock
generated
6
flake.lock
generated
@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1726755586,
|
"lastModified": 1711703276,
|
||||||
"narHash": "sha256-PmUr/2GQGvFTIJ6/Tvsins7Q43KTMvMFhvG6oaYK+Wk=",
|
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "c04d5652cfa9742b1d519688f65d1bbccea9eb7e",
|
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
18
flake.nix
18
flake.nix
@ -57,28 +57,14 @@
|
|||||||
nativeBuildInputs = [ poetry-core ];
|
nativeBuildInputs = [ poetry-core ];
|
||||||
propagatedBuildInputs = [ setuptools flakes.self.pkgs.remi requests typer rich urllib3 ];
|
propagatedBuildInputs = [ setuptools flakes.self.pkgs.remi requests typer rich urllib3 ];
|
||||||
};
|
};
|
||||||
grzegorzctl = pkgs.runCommandNoCCLocal "grzegorzctl" (
|
default = flakes.self.pkgs.grzegorz-clients;
|
||||||
{
|
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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";
|
||||||
grzegorzctl.type = "app";
|
grzegorzctl.type = "app";
|
||||||
grzegorzctl.program = "${self.packages.${system}.grzegorzctl}/bin/grzegorzctl";
|
grzegorzctl.program = "${self.packages.${system}.grzegorz-clients}/bin/grzegorzctl";
|
||||||
default = grzegorzctl;
|
default = grzegorzctl;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ def _add(
|
|||||||
api.playlist_goto(current_index if put_pre else current_index + 1)
|
api.playlist_goto(current_index if put_pre else current_index + 1)
|
||||||
api.set_playing(True)
|
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(
|
def play(
|
||||||
urls: list[str],
|
urls: list[str],
|
||||||
pre: bool = False,
|
pre: bool = False,
|
||||||
@ -72,14 +72,14 @@ def play(
|
|||||||
):
|
):
|
||||||
_add(urls, put_post=not pre, put_pre=pre, play=True, api_base=api_base)
|
_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(
|
def next(
|
||||||
urls: list[str],
|
urls: list[str],
|
||||||
api_base: str = DEFAULT_API_BASE,
|
api_base: str = DEFAULT_API_BASE,
|
||||||
):
|
):
|
||||||
_add(urls, put_post=True, api_base=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(
|
def queue(
|
||||||
urls: list[str],
|
urls: list[str],
|
||||||
play: bool = True,
|
play: bool = True,
|
||||||
@ -107,12 +107,6 @@ def pause( api_base: str = DEFAULT_API_BASE ):
|
|||||||
api.set_endpoint(api_base)
|
api.set_endpoint(api_base)
|
||||||
rich.print(api.set_playing(False), file=sys.stderr)
|
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")
|
@cli.command(help="Goto next item in playlist")
|
||||||
def skip( api_base: str = DEFAULT_API_BASE ):
|
def skip( api_base: str = DEFAULT_API_BASE ):
|
||||||
api.set_endpoint(api_base)
|
api.set_endpoint(api_base)
|
||||||
@ -160,21 +154,11 @@ def status(
|
|||||||
|
|
||||||
@cli.command(help="Set the playback volume")
|
@cli.command(help="Set the playback volume")
|
||||||
def set_volume(
|
def set_volume(
|
||||||
volume: str,
|
volume: int,
|
||||||
api_base: str = DEFAULT_API_BASE,
|
api_base: str = DEFAULT_API_BASE,
|
||||||
):
|
):
|
||||||
api.set_endpoint(api_base)
|
api.set_endpoint(api_base)
|
||||||
|
rich.print(api.set_volume(volume), file=sys.stderr)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
23
grzegorz_clients/gzregorz.tcss
Normal file
23
grzegorz_clients/gzregorz.tcss
Normal 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%;
|
||||||
|
}
|
@ -294,7 +294,7 @@ class RemiApp(App):
|
|||||||
except api.APIError:
|
except api.APIError:
|
||||||
playback_pos = None
|
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
|
slider_pos = playback_pos["current"] / playback_pos["total"] * 100
|
||||||
current = seconds_to_timestamp(playback_pos["current"])
|
current = seconds_to_timestamp(playback_pos["current"])
|
||||||
total = seconds_to_timestamp(playback_pos["total"])
|
total = seconds_to_timestamp(playback_pos["total"])
|
||||||
|
172
grzegorz_clients/tui.py
Normal file
172
grzegorz_clients/tui.py
Normal 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
1259
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -6,19 +6,22 @@ authors = ["Peder Bergebakken Sundt <pbsds@hotmail.com>"]
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.7,<4.0"
|
python = ">=3.8.1,<4.0"
|
||||||
remi = "1.0"
|
remi = "1.0"
|
||||||
requests = ">=2.27.1,<3"
|
requests = ">=2.27.1,<3"
|
||||||
rich = ">=13.3.5"
|
rich = ">=13.3.5"
|
||||||
typer = ">=0.4.0"
|
typer = ">=0.4.0"
|
||||||
urllib3 = ">=1.26.8,<3"
|
urllib3 = ">=1.26.8,<3"
|
||||||
|
textual = {version = "^0.61.0", extras = ["tui"]}
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
python-lsp-server = {extras = ["all"], version = "^1.5.0"}
|
python-lsp-server = {extras = ["all"], version = "^1.11.0"}
|
||||||
|
textual-dev = "^1.5.1"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
grzegorz-webui = "grzegorz_clients.__main__:cli"
|
grzegorz-webui = "grzegorz_clients.__main__:cli"
|
||||||
grzegorzctl = "grzegorz_clients.cli:cli"
|
grzegorzctl = "grzegorz_clients.cli:cli"
|
||||||
|
grzegorztui = "grzegorz_clients.tui:main"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
Loading…
Reference in New Issue
Block a user