Changes to internal config handling, server side checking of waiting room policy, updatable config
This commit is contained in:
parent
a983c74de8
commit
31c45e3fe4
2 changed files with 212 additions and 69 deletions
|
@ -62,6 +62,14 @@ sources: dict[str, Source] = {}
|
|||
currentLock: asyncio.Semaphore = asyncio.Semaphore(0)
|
||||
|
||||
|
||||
def default_config():
|
||||
return {
|
||||
"preview_duration": 3,
|
||||
"last_song": None,
|
||||
"waiting_room_policy": None,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class State:
|
||||
"""This captures the current state of the playback client.
|
||||
|
@ -87,12 +95,19 @@ class State:
|
|||
:type secret: str
|
||||
:param key: An optional key, if registration on the server is limited.
|
||||
:type key: Optional[str]
|
||||
:param preview_duration: Amount of seconds the preview before a song be
|
||||
displayed.
|
||||
:type preview_duration: int
|
||||
:param last_song: At what time should the server not accept any more songs.
|
||||
`None` if no such limit should exist.
|
||||
:type last_song: Optional[datetime.datetime]
|
||||
:param config: Various configuration options for the client:
|
||||
* `preview_duration` (`Optional[int]`): The duration in seconds the
|
||||
playback client shows a preview for the next song. This is accounted for
|
||||
in the calculation of the ETA for songs later in the queue.
|
||||
* `last_song` (`Optional[datetime.datetime]`): A timestamp, defining the end of
|
||||
the queue.
|
||||
* `waiting_room_policy` (Optional[str]): One of:
|
||||
- `force`, if a performer is already in the queue, they are put in the
|
||||
waiting room.
|
||||
- `optional`, if a performer is already in the queue, they have the option
|
||||
to be put in the waiting room.
|
||||
- `None`, performers are always added to the queue.
|
||||
:type config: dict[str, Any]:
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
@ -105,31 +120,17 @@ class State:
|
|||
server: str = ""
|
||||
secret: str = ""
|
||||
key: Optional[str] = None
|
||||
preview_duration: int = 3
|
||||
last_song: Optional[datetime.datetime] = None
|
||||
|
||||
def get_config(self) -> dict[str, Any]:
|
||||
"""
|
||||
Return a subset of values to be send to the server.
|
||||
|
||||
Currently this is:
|
||||
- :py:attr:`State.preview_duration`
|
||||
- :py:attr:`State.last_song` (As a timestamp)
|
||||
|
||||
:return: A dict resulting from the above values
|
||||
:rtype: dict[str, Any]
|
||||
"""
|
||||
return {
|
||||
"preview_duration": self.preview_duration,
|
||||
"last_song": self.last_song.timestamp()
|
||||
if self.last_song
|
||||
else None,
|
||||
}
|
||||
config: dict[str, Any] = field(default_factory=default_config)
|
||||
|
||||
|
||||
state: State = State()
|
||||
|
||||
|
||||
@sio.on("update_config")
|
||||
async def handle_update_config(data: dict[str, Any]) -> None:
|
||||
state.config = default_config() | data
|
||||
|
||||
|
||||
@sio.on("skip-current")
|
||||
async def handle_skip_current(data: dict[str, Any]) -> None:
|
||||
"""
|
||||
|
@ -201,7 +202,7 @@ async def handle_connect() -> None:
|
|||
"recent": state.recent,
|
||||
"room": state.room,
|
||||
"secret": state.secret,
|
||||
"config": state.get_config(),
|
||||
"config": state.config,
|
||||
}
|
||||
if state.key:
|
||||
data["registration-key"] = state.key
|
||||
|
@ -252,7 +253,7 @@ async def preview(entry: Entry) -> None:
|
|||
process = await asyncio.create_subprocess_exec(
|
||||
"mpv",
|
||||
tmpfile.name,
|
||||
f"--image-display-duration={state.preview_duration}",
|
||||
f"--image-display-duration={state.config['preview_duration']}",
|
||||
"--sub-pos=50",
|
||||
"--sub-file=-",
|
||||
"--fullscreen",
|
||||
|
@ -290,7 +291,7 @@ async def handle_play(data: dict[str, Any]) -> None:
|
|||
)
|
||||
try:
|
||||
state.current_source = sources[entry.source]
|
||||
if state.preview_duration > 0:
|
||||
if state.config["preview_duration"] > 0:
|
||||
await preview(entry)
|
||||
await sources[entry.source].play(entry)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
|
@ -409,12 +410,12 @@ async def aiomain() -> None:
|
|||
sources.update(configure_sources(config["sources"]))
|
||||
|
||||
if "config" in config:
|
||||
if "last_song" in config["config"]:
|
||||
state.last_song = datetime.datetime.fromisoformat(
|
||||
config["config"]["last_song"]
|
||||
)
|
||||
if "preview_duration" in config["config"]:
|
||||
state.preview_duration = config["config"]["preview_duration"]
|
||||
last_song = (
|
||||
datetime.datetime.fromisoformat(config["config"]["last_song"])
|
||||
if "last_song" in config["config"]
|
||||
else None
|
||||
)
|
||||
state.config |= config["config"] | {"last_song": last_song}
|
||||
|
||||
state.key = args.key if args.key else None
|
||||
|
||||
|
|
210
syng/server.py
210
syng/server.py
|
@ -21,6 +21,7 @@ import logging
|
|||
import os
|
||||
import random
|
||||
import string
|
||||
from json.decoder import JSONDecodeError
|
||||
from argparse import ArgumentParser
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
|
@ -43,6 +44,12 @@ sio = socketio.AsyncServer(
|
|||
app = web.Application()
|
||||
sio.attach(app)
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"preview_duration": 3,
|
||||
"waiting_room_policy": None,
|
||||
"last_song": None,
|
||||
}
|
||||
|
||||
|
||||
async def root_handler(request: Any) -> Any:
|
||||
"""
|
||||
|
@ -68,7 +75,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
class Client:
|
||||
"""This stores the configuration of a specific playback client.
|
||||
|
||||
In case a new playback client connects to a room, these values can be
|
||||
|
@ -79,18 +86,23 @@ class Config:
|
|||
:type sources: Source
|
||||
:param sources_prio: A list defining the order of the search results.
|
||||
:type sources_prio: list[str]
|
||||
:param preview_duration: The duration in seconds the playbackclients shows
|
||||
a preview for the next song. This is accounted for in the calculation
|
||||
of the ETA for songs later in the queue.
|
||||
:type preview_duration: int
|
||||
:param last_song: A timestamp, defining the end of the queue.
|
||||
:type last_song: Optional[float]
|
||||
:param config: Various configuration options for the client:
|
||||
* `preview_duration` (`Optional[int]`): The duration in seconds the
|
||||
playback client shows a preview for the next song. This is accounted for
|
||||
in the calculation of the ETA for songs later in the queue.
|
||||
* `last_song` (`Optional[float]`): A timestamp, defining the end of the queue.
|
||||
* `waiting_room_policy` (Optional[str]): One of:
|
||||
- `force`, if a performer is already in the queue, they are put in the
|
||||
waiting room.
|
||||
- `optional`, if a performer is already in the queue, they have the option
|
||||
to be put in the waiting room.
|
||||
- `None`, performers are always added to the queue.
|
||||
:type config: dict[str, Any]:
|
||||
"""
|
||||
|
||||
sources: dict[str, Source]
|
||||
sources_prio: list[str]
|
||||
preview_duration: int
|
||||
last_song: Optional[float]
|
||||
config: dict[str, Any]
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -113,8 +125,8 @@ class State:
|
|||
a new playback client connects to a room (with the correct secret),
|
||||
this will be swapped with the new sid.
|
||||
:type sid: str
|
||||
:param config: The config for the client
|
||||
:type config: Config
|
||||
:param client: The config for the playback client
|
||||
:type client: Client
|
||||
"""
|
||||
|
||||
secret: str
|
||||
|
@ -122,7 +134,7 @@ class State:
|
|||
waiting_room: list[Entry]
|
||||
recent: list[Entry]
|
||||
sid: str
|
||||
config: Config
|
||||
client: Client
|
||||
last_seen: datetime.datetime = field(
|
||||
init=False, default_factory=datetime.datetime.now
|
||||
)
|
||||
|
@ -153,6 +165,7 @@ async def send_state(state: State, sid: str) -> None:
|
|||
"queue": state.queue,
|
||||
"recent": state.recent,
|
||||
"waiting_room": state.waiting_room,
|
||||
"config": state.client.config,
|
||||
},
|
||||
room=sid,
|
||||
)
|
||||
|
@ -188,7 +201,7 @@ async def handle_waiting_room_append(sid: str, data: dict[str, Any]) -> None:
|
|||
room = session["room"]
|
||||
state = clients[room]
|
||||
|
||||
source_obj = state.config.sources[data["source"]]
|
||||
source_obj = state.client.sources[data["source"]]
|
||||
|
||||
entry = await source_obj.get_entry(data["performer"], data["ident"])
|
||||
|
||||
|
@ -254,18 +267,23 @@ async def append_to_queue(
|
|||
start_time = state.queue.fold(
|
||||
lambda item, time: time
|
||||
+ item.duration
|
||||
+ state.config.preview_duration
|
||||
+ state.client.config["preview_duration"]
|
||||
+ 1,
|
||||
start_time,
|
||||
)
|
||||
|
||||
if state.config.last_song:
|
||||
if state.config.last_song < start_time:
|
||||
end_time = datetime.datetime.fromtimestamp(state.config.last_song)
|
||||
if state.client.config["last_song"]:
|
||||
if state.client.config["last_song"] < start_time:
|
||||
# end_time = datetime.datetime.fromtimestamp(
|
||||
# state.client.config["last_song"]
|
||||
# )
|
||||
if report_to is not None:
|
||||
await sio.emit(
|
||||
"err",
|
||||
{"type": "QUEUE_FULL", "end_time": state.config.last_song},
|
||||
{
|
||||
"type": "QUEUE_FULL",
|
||||
"end_time": state.client.config["last_song"],
|
||||
},
|
||||
room=report_to,
|
||||
)
|
||||
return
|
||||
|
@ -280,6 +298,57 @@ async def append_to_queue(
|
|||
)
|
||||
|
||||
|
||||
@sio.on("show_config")
|
||||
async def handle_show_config(sid: str) -> None:
|
||||
"""
|
||||
Sends public config to webclient.
|
||||
|
||||
This will only be send if the client is on an admin connection.
|
||||
|
||||
:param sid: The session id of the client sending this request
|
||||
:type sid: str
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
async with sio.session(sid) as session:
|
||||
room = session["room"]
|
||||
is_admin = session["admin"]
|
||||
state = clients[room]
|
||||
|
||||
if is_admin:
|
||||
await sio.emit(
|
||||
"config",
|
||||
state.client.config,
|
||||
sid,
|
||||
)
|
||||
else:
|
||||
await sio.emit("err", {"type": "NO_ADMIN"}, sid)
|
||||
|
||||
|
||||
@sio.on("update_config")
|
||||
async def handle_update_config(sid: str, data: dict[str, Any]) -> None:
|
||||
async with sio.session(sid) as session:
|
||||
room = session["room"]
|
||||
is_admin = session["admin"]
|
||||
state = clients[room]
|
||||
|
||||
if is_admin:
|
||||
try:
|
||||
config = json.loads(data["config"])
|
||||
await sio.emit(
|
||||
"update_config",
|
||||
DEFAULT_CONFIG | config,
|
||||
state.sid,
|
||||
)
|
||||
state.client.config = DEFAULT_CONFIG | config
|
||||
await sio.emit("update_config", config, room)
|
||||
except JSONDecodeError:
|
||||
await sio.emit("err", {"type": "JSON_MALFORMED"})
|
||||
|
||||
else:
|
||||
await sio.emit("err", {"type": "NO_ADMIN"}, sid)
|
||||
|
||||
|
||||
@sio.on("append")
|
||||
async def handle_append(sid: str, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
|
@ -297,6 +366,10 @@ async def handle_append(sid: str, data: dict[str, Any]) -> None:
|
|||
request is denied and a "msg" message is send to the client, detailing
|
||||
this.
|
||||
|
||||
If a waitingroom is forced or optional, it is checked, if one of the performers is
|
||||
already in queue. In that case, a "ask_for_waitingroom" message is send to the
|
||||
client.
|
||||
|
||||
Otherwise the song is added to the queue. And all connected clients (web
|
||||
and playback client) are informed of the new state with a "state" message.
|
||||
|
||||
|
@ -316,7 +389,30 @@ async def handle_append(sid: str, data: dict[str, Any]) -> None:
|
|||
room = session["room"]
|
||||
state = clients[room]
|
||||
|
||||
source_obj = state.config.sources[data["source"]]
|
||||
if state.client.config["waiting_room_policy"] and (
|
||||
state.client.config["waiting_room_policy"].lower() == "force"
|
||||
or state.client.config["waiting_room_policy"].lower() == "optional"
|
||||
):
|
||||
old_entry = state.queue.find_by_name(data["performer"])
|
||||
if old_entry is not None:
|
||||
await sio.emit(
|
||||
"ask_for_waitingroom",
|
||||
{
|
||||
"current_entry": {
|
||||
"source": data["source"],
|
||||
"performer": data["performer"],
|
||||
"ident": data["ident"],
|
||||
},
|
||||
"old_entry": {
|
||||
"artist": old_entry.artist,
|
||||
"title": old_entry.title,
|
||||
"performer": old_entry.performer,
|
||||
},
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
source_obj = state.client.sources[data["source"]]
|
||||
|
||||
entry = await source_obj.get_entry(data["performer"], data["ident"])
|
||||
|
||||
|
@ -333,6 +429,47 @@ async def handle_append(sid: str, data: dict[str, Any]) -> None:
|
|||
await append_to_queue(room, entry, sid)
|
||||
|
||||
|
||||
@sio.on("append-anyway")
|
||||
async def handle_append_anyway(sid: str, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Appends a song to the queue, even if the performer is already in queue.
|
||||
|
||||
Works the same as handle_append, but without the check if the performer is already
|
||||
in queue.
|
||||
|
||||
Only if the waiting_room_policy is not configured as forced.
|
||||
"""
|
||||
async with sio.session(sid) as session:
|
||||
room = session["room"]
|
||||
state = clients[room]
|
||||
|
||||
if state.client.config["waiting_room_policy"].lower() == "force":
|
||||
await sio.emit(
|
||||
"err",
|
||||
{"type": "WAITING_ROOM_FORCED"},
|
||||
room=sid,
|
||||
)
|
||||
return
|
||||
|
||||
source_obj = state.client.sources[data["source"]]
|
||||
|
||||
entry = await source_obj.get_entry(data["performer"], data["ident"])
|
||||
|
||||
if entry is None:
|
||||
await sio.emit(
|
||||
"msg",
|
||||
{"msg": f"Unable to append {data['ident']}. Maybe try again?"},
|
||||
room=sid,
|
||||
)
|
||||
return
|
||||
|
||||
entry.uid = data["uid"] if "uid" in data else None
|
||||
|
||||
print(entry)
|
||||
|
||||
await append_to_queue(room, entry, sid)
|
||||
|
||||
|
||||
@sio.on("meta-info")
|
||||
async def handle_meta_info(sid: str, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
|
@ -586,10 +723,10 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None:
|
|||
if data["secret"] == old_state.secret:
|
||||
logger.info("Got new client connection for %s", room)
|
||||
old_state.sid = sid
|
||||
old_state.config = Config(
|
||||
sources=old_state.config.sources,
|
||||
sources_prio=old_state.config.sources_prio,
|
||||
**data["config"],
|
||||
old_state.client = Client(
|
||||
sources=old_state.client.sources,
|
||||
sources_prio=old_state.client.sources_prio,
|
||||
config=DEFAULT_CONFIG | data["config"],
|
||||
)
|
||||
await sio.enter_room(sid, room)
|
||||
await sio.emit(
|
||||
|
@ -615,8 +752,13 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None:
|
|||
waiting_room=initial_waiting_room,
|
||||
recent=initial_recent,
|
||||
sid=sid,
|
||||
config=Config(sources={}, sources_prio=[], **data["config"]),
|
||||
client=Client(
|
||||
sources={},
|
||||
sources_prio=[],
|
||||
config=DEFAULT_CONFIG | data["config"],
|
||||
),
|
||||
)
|
||||
|
||||
await sio.enter_room(sid, room)
|
||||
await sio.emit(
|
||||
"client-registered", {"success": True, "room": room}, room=sid
|
||||
|
@ -653,13 +795,13 @@ async def handle_sources(sid: str, data: dict[str, Any]) -> None:
|
|||
if sid != state.sid:
|
||||
return
|
||||
|
||||
unused_sources = state.config.sources.keys() - data["sources"]
|
||||
new_sources = data["sources"] - state.config.sources.keys()
|
||||
unused_sources = state.client.sources.keys() - data["sources"]
|
||||
new_sources = data["sources"] - state.client.sources.keys()
|
||||
|
||||
for source in unused_sources:
|
||||
del state.config.sources[source]
|
||||
del state.client.sources[source]
|
||||
|
||||
state.config.sources_prio = data["sources"]
|
||||
state.client.sources_prio = data["sources"]
|
||||
|
||||
for name in new_sources:
|
||||
await sio.emit("request-config", {"source": name}, room=sid)
|
||||
|
@ -690,12 +832,12 @@ async def handle_config_chunk(sid: str, data: dict[str, Any]) -> None:
|
|||
if sid != state.sid:
|
||||
return
|
||||
|
||||
if not data["source"] in state.config.sources:
|
||||
state.config.sources[data["source"]] = available_sources[
|
||||
if data["source"] not in state.client.sources:
|
||||
state.client.sources[data["source"]] = available_sources[
|
||||
data["source"]
|
||||
](data["config"])
|
||||
else:
|
||||
state.config.sources[data["source"]].add_to_config(data["config"])
|
||||
state.client.sources[data["source"]].add_to_config(data["config"])
|
||||
|
||||
|
||||
@sio.on("config")
|
||||
|
@ -722,7 +864,7 @@ async def handle_config(sid: str, data: dict[str, Any]) -> None:
|
|||
if sid != state.sid:
|
||||
return
|
||||
|
||||
state.config.sources[data["source"]] = available_sources[data["source"]](
|
||||
state.client.sources[data["source"]] = available_sources[data["source"]](
|
||||
data["config"]
|
||||
)
|
||||
|
||||
|
@ -910,8 +1052,8 @@ async def handle_search(sid: str, data: dict[str, Any]) -> None:
|
|||
query = data["query"]
|
||||
results_list = await asyncio.gather(
|
||||
*[
|
||||
state.config.sources[source].search(query)
|
||||
for source in state.config.sources_prio
|
||||
state.client.sources[source].search(query)
|
||||
for source in state.client.sources_prio
|
||||
]
|
||||
)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue