Compare commits
12 Commits
dependabot
...
tui
| Author | SHA1 | Date | |
|---|---|---|---|
| d716b5628a | |||
| 76aaaa0a45 | |||
| e00ae9e344 | |||
| 0536a56ca6 | |||
| a9e8330898 | |||
|
3059898e38
|
|||
| 738a4f3dd8 | |||
| 7e8baa0a48 | |||
| f3cbb43f91 | |||
| c38f2f22a6 | |||
| b23b02b5e8 | |||
| 99f41e54c4 |
2
.envrc
2
.envrc
@@ -3,7 +3,7 @@
|
||||
# It enters you into the poetry venv, removing the need for `poetry run`.
|
||||
|
||||
if command -v nix >/dev/null; then
|
||||
use nix -p poetry
|
||||
use flake || use nix -p poetry
|
||||
fi
|
||||
|
||||
# Instead of using the flake, we use poetry to manage a development venv
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ __pycache__/
|
||||
*.pyc
|
||||
result
|
||||
result-*
|
||||
.direnv/
|
||||
|
||||
12
README.md
12
README.md
@@ -5,18 +5,26 @@ 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
|
||||
grzegorz-webui --host 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. ;)
|
||||
|
||||
|
||||
4
dev.sh
4
dev.sh
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
if ! which entr > /dev/null; then
|
||||
#!/usr/bin/env bash
|
||||
if ! command -v entr > /dev/null; then
|
||||
echo "entr is not installed, aborting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1692174805,
|
||||
"narHash": "sha256-xmNPFDi/AUMIxwgOH/IVom55Dks34u1g7sFKKebxUm0=",
|
||||
"lastModified": 1711703276,
|
||||
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "caac0eb6bdcad0b32cb2522e03e4002c8975c62e",
|
||||
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
18
flake.nix
18
flake.nix
@@ -30,6 +30,8 @@
|
||||
#"riscv64-linux"
|
||||
];
|
||||
in {
|
||||
inherit inputs;
|
||||
|
||||
packages = forAllSystems ({ pkgs, flakes, ...}: {
|
||||
remi = with pkgs.python3.pkgs; buildPythonPackage rec {
|
||||
pname = "remi";
|
||||
@@ -53,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;
|
||||
});
|
||||
@@ -61,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
|
||||
@@ -126,5 +130,15 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
devShells = forAllSystems ({ pkgs, ... }: rec {
|
||||
default = pkgs.mkShellNoCC {
|
||||
packages = [
|
||||
pkgs.poetry
|
||||
pkgs.python3
|
||||
pkgs.entr
|
||||
];
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,40 +11,88 @@ def set_endpoint(base_url:str):
|
||||
# Exceptions:
|
||||
class APIError(Exception): pass
|
||||
|
||||
def parse_message(
|
||||
method: str,
|
||||
url: str,
|
||||
status: str,
|
||||
function_name: str,
|
||||
json_text: str,
|
||||
is_get: bool,
|
||||
) -> dict[str, any]:
|
||||
prefix = f"[{function_name}] {method} /{url} -> {status}:"
|
||||
try:
|
||||
data = json.loads(json_text)
|
||||
except json.JSONDecodeError:
|
||||
raise APIError(f"{prefix} Expected json response, got:\n{json_text}")
|
||||
|
||||
if type(data) is not dict:
|
||||
raise APIError(f"{prefix} Expected json response to be a dict, got:\n{json_text}")
|
||||
|
||||
if "error" not in data:
|
||||
raise APIError(f"{prefix} Missing json data 'error', got:\n{json_text}")
|
||||
|
||||
if data["error"] != False:
|
||||
raise APIError(f"{prefix} Got error {str(data['error'])}, got:\n{json_text}")
|
||||
|
||||
if not is_get and "success" not in data:
|
||||
raise APIError(f"{prefix} Missing json data 'success', got:\n{json_text}")
|
||||
|
||||
return data
|
||||
|
||||
# decorator:
|
||||
# (TODO): Add logging
|
||||
def request_delete(func):
|
||||
@wraps(func)
|
||||
def new_func(*args, **kwargs):
|
||||
url, data = func(*args, **kwargs)
|
||||
if type(data) is dict: data = json.dumps(data)
|
||||
response = requests.delete(f"{BASE_URL}/{url}", data=data)
|
||||
data = json.loads(response.text)
|
||||
if "error" not in data or data["error"] != False:
|
||||
print(data)
|
||||
raise APIError(data["error"])
|
||||
response = requests.delete(f"{BASE_URL}/{url}", json=data)
|
||||
response.raise_for_status() # raises HTTPError, if any
|
||||
data = parse_message(
|
||||
"DELETE",
|
||||
url,
|
||||
response.status_code,
|
||||
func.__name__,
|
||||
response.text,
|
||||
is_get = False,
|
||||
)
|
||||
return data["success"]
|
||||
return new_func
|
||||
|
||||
def request_post(func):
|
||||
@wraps(func)
|
||||
def new_func(*args, **kwargs):
|
||||
url, data = func(*args, **kwargs)
|
||||
if type(data) is dict: data = json.dumps(data)
|
||||
response = requests.post(f"{BASE_URL}/{url}", data=data)
|
||||
data = json.loads(response.text)
|
||||
if "error" not in data or data["error"] != False:
|
||||
print(data)
|
||||
raise APIError(data["error"])
|
||||
response = requests.post(f"{BASE_URL}/{url}", json=data)
|
||||
response.raise_for_status() # raises HTTPError, if any
|
||||
data = parse_message(
|
||||
"POST",
|
||||
url,
|
||||
response.status_code,
|
||||
func.__name__,
|
||||
response.text,
|
||||
is_get = False,
|
||||
)
|
||||
return data["success"]
|
||||
return new_func
|
||||
|
||||
def request_get(func):
|
||||
@wraps(func)
|
||||
def new_func(*args, **kwargs):
|
||||
url = func(*args, **kwargs)
|
||||
response = requests.get(f"{BASE_URL}/{url}")
|
||||
data = json.loads(response.text)
|
||||
if "error" not in data or data["error"] != False:
|
||||
raise APIError(data["errortext"])
|
||||
response.raise_for_status() # raises HTTPError, if any
|
||||
data = parse_message(
|
||||
"GET",
|
||||
url,
|
||||
response.status_code,
|
||||
func.__name__,
|
||||
response.text,
|
||||
is_get = True,
|
||||
)
|
||||
|
||||
if "value" not in data:
|
||||
raise APIError(f"[{func.__name__}] Missing json data 'value', got:\n{json.dumps(data)}")
|
||||
|
||||
return data["value"]
|
||||
return new_func
|
||||
|
||||
@@ -69,7 +117,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 +164,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"
|
||||
|
||||
165
grzegorz_clients/cli.py
Normal file
165
grzegorz_clients/cli.py
Normal file
@@ -0,0 +1,165 @@
|
||||
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)
|
||||
|
||||
def _add(
|
||||
urls : list[str],
|
||||
put_pre : bool = False,
|
||||
put_post : bool = False,
|
||||
play : bool = False,
|
||||
api_base : str = DEFAULT_API_BASE,
|
||||
):
|
||||
api.set_endpoint(api_base)
|
||||
if put_pre or put_post:
|
||||
pre = api.get_playlist()
|
||||
|
||||
for url in urls:
|
||||
resp = api.load_path(url)
|
||||
rich.print(f"{url} : {resp!r}", file=sys.stderr)
|
||||
|
||||
if put_pre or put_post:
|
||||
assert put_pre != put_post
|
||||
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 put_pre else current_index + 1
|
||||
if target not in new_indices:
|
||||
for idx in sorted(new_indices):
|
||||
api.playlist_move(idx, target)
|
||||
target += 1
|
||||
|
||||
if play:
|
||||
assert put_pre or put_post
|
||||
api.playlist_goto(current_index if put_pre else current_index + 1)
|
||||
api.set_playing(True)
|
||||
|
||||
@cli.command(help="Add one ore more items to the playlist")
|
||||
def play(
|
||||
urls: list[str],
|
||||
pre: bool = False,
|
||||
api_base: str = DEFAULT_API_BASE,
|
||||
):
|
||||
_add(urls, put_post=not pre, put_pre=pre, play=True, api_base=api_base)
|
||||
|
||||
@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 ore more items to the playlist")
|
||||
def queue(
|
||||
urls: list[str],
|
||||
play: bool = True,
|
||||
api_base: str = DEFAULT_API_BASE,
|
||||
):
|
||||
_add(urls, api_base=api_base)
|
||||
if play:
|
||||
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 to True")
|
||||
def resume( 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 skip( 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: int,
|
||||
api_base: str = DEFAULT_API_BASE,
|
||||
):
|
||||
api.set_endpoint(api_base)
|
||||
rich.print(api.set_volume(volume), file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
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%;
|
||||
}
|
||||
@@ -320,6 +320,16 @@ class RemiApp(App):
|
||||
playlist = api.get_playlist() # json structure
|
||||
N = len(playlist)
|
||||
|
||||
start_ellipsis = False
|
||||
end_ellipsis = False
|
||||
if N > 100:
|
||||
current, *_ = *(i for i, playlist_item in enumerate(playlist) if playlist_item.get("current", False)), None
|
||||
if current is not None:
|
||||
playlist = playlist[max(0, current - 50) : max(current+50, 100)]
|
||||
start_ellipsis = current - 50 > 0
|
||||
end_ellipsis = max(current+50, 100) < N
|
||||
|
||||
|
||||
# update playlist table content:
|
||||
table = []
|
||||
for playlist_item in playlist:
|
||||
@@ -340,6 +350,11 @@ class RemiApp(App):
|
||||
icons.TRASH,
|
||||
])
|
||||
|
||||
if start_ellipsis:
|
||||
table.insert(0, ["", f"...{current - 50} more ...", "", "", "", "", ""])
|
||||
if end_ellipsis:
|
||||
table.append(["", f"...{N - current - 50} more ...", "", "", "", "", ""])
|
||||
|
||||
this_playlist = list(zip(table, [i.get("current", False) for i in playlist])) # ew, but it works...
|
||||
if this_playlist == self.old_playlist: return
|
||||
self.old_playlist = this_playlist
|
||||
@@ -349,7 +364,7 @@ class RemiApp(App):
|
||||
|
||||
# styling the new table:
|
||||
# for each row element:
|
||||
for row_key, playlist_item in zip(self.playlist.table._render_children_list[1:], playlist):
|
||||
for row_key, playlist_item in zip(self.playlist.table._render_children_list[1:][1 if start_ellipsis else 0 : -1 if end_ellipsis else None], playlist):
|
||||
row_widget = self.playlist.table.get_child(row_key)
|
||||
row_widget.set_on_click_listener(self.on_table_row_click, playlist_item)
|
||||
|
||||
@@ -399,6 +414,7 @@ class RemiApp(App):
|
||||
|
||||
#print(index, key, item_widget)
|
||||
|
||||
|
||||
def set_playing(self, is_playing:bool): # Only updates GUI elements!
|
||||
self.playback.play.set_text(icons.PAUSE if is_playing else icons.PLAY)
|
||||
self.playback.seek_slider.set_enabled(is_playing)
|
||||
|
||||
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()
|
||||
1605
poetry.lock
generated
1605
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -6,17 +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"
|
||||
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"]
|
||||
|
||||
Reference in New Issue
Block a user