diff --git a/syng/client.py b/syng/client.py index ad2ceb9..603e31e 100644 --- a/syng/client.py +++ b/syng/client.py @@ -75,6 +75,8 @@ class State: :type current_source: Optional[Source] :param queue: A copy of the current playlist on the server. :type queue: list[Entry] + :param waiting_room: A copy of the waiting room on the server. + :type waiting_room: list[Entry] :param recent: A copy of all played songs this session. :type recent: list[Entry] :param room: The room on the server this playback client is connected to. @@ -97,6 +99,7 @@ class State: current_source: Optional[Source] = None queue: list[Entry] = field(default_factory=list) + waiting_room: list[Entry] = field(default_factory=list) recent: list[Entry] = field(default_factory=list) room: str = "" server: str = "" @@ -164,6 +167,7 @@ async def handle_state(data: dict[str, Any]) -> None: :rtype: None """ state.queue = [Entry(**entry) for entry in data["queue"]] + state.waiting_room = [Entry(**entry) for entry in data["waiting_room"]] state.recent = [Entry(**entry) for entry in data["recent"]] for entry in state.queue[:2]: @@ -193,6 +197,7 @@ async def handle_connect() -> None: logging.info("Connected to server") data = { "queue": state.queue, + "waiting_room": state.waiting_room, "recent": state.recent, "room": state.room, "secret": state.secret, @@ -200,6 +205,7 @@ async def handle_connect() -> None: } if state.key: data["registration-key"] = state.key + print(data) await sio.emit("register-client", data) diff --git a/syng/queue.py b/syng/queue.py index d7e54e6..3a95454 100644 --- a/syng/queue.py +++ b/syng/queue.py @@ -1,8 +1,8 @@ """A async queue with synchronization.""" import asyncio from collections import deque +from collections.abc import Callable, Iterable from typing import Any -from typing import Callable from typing import Optional from uuid import UUID @@ -95,6 +95,8 @@ class Queue: """ Update entries in the queue, identified by their uuid. + If an entry with that uuid is not in the queue, nothing happens. + :param uuid: The uuid of the entry to update :type uuid: UUID | str :param updater: A function, that updates the entry @@ -119,6 +121,15 @@ class Queue: return item return None + def find_by_uid(self, uid: str) -> Iterable[Entry]: + """ + Find all entries for a given user id + """ + + for item in self._queue: + if item.uid == uid: + yield item + def fold(self, func: Callable[[Entry, Any], Any], start_value: Any) -> Any: """Call ``func`` on each entry and accumulate the result.""" for item in self._queue: diff --git a/syng/server.py b/syng/server.py index 3165d47..235a795 100644 --- a/syng/server.py +++ b/syng/server.py @@ -104,6 +104,9 @@ class State: are appended to this, and if a playback client requests a song, it is taken from the top. :type queue: Queue + :param waiting_room: Contains the Entries, that are hold back, until a + specific song is finished. + :type waiting_room: list[Entry] :param recent: A list of already played songs in order. :type recent: list[Entry] :param sid: The socket.io session id of the (unique) playback client. Once @@ -116,6 +119,7 @@ class State: secret: str queue: Queue + waiting_room: list[Entry] recent: list[Entry] sid: str config: Config @@ -145,7 +149,11 @@ async def send_state(state: State, sid: str) -> None: """ await sio.emit( "state", - {"queue": state.queue, "recent": state.recent}, + { + "queue": state.queue, + "recent": state.recent, + "waiting_room": state.waiting_room, + }, room=sid, ) @@ -170,6 +178,85 @@ async def handle_state(sid: str) -> None: await send_state(state, sid) +@sio.on("waiting-room-append") +async def handle_waiting_room_append(sid: str, data: dict[str, Any]) -> None: + async with sio.session(sid) as session: + room = session["room"] + state = clients[room] + + print(data) + + source_obj = state.config.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 add to the waiting room: {data['ident']}"}, + ) + return + + if ( + "uid" not in data + or len(list(state.queue.find_by_uid(data["uid"]))) == 0 + ): + await append_to_queue(room, entry, sid) + return + + entry.uid = data["uid"] + + state.waiting_room.append(entry) + print(state.waiting_room) + await send_state(state, room) + await sio.emit( + "get-meta-info", + entry, + room=clients[room].sid, + ) + + # Und jetzt iwie hinzufügen, oder direkt queuen :/ + + +async def append_to_queue(room, entry, report_to=None): + state = clients[room] + + first_song = state.queue.try_peek() + if first_song is None or first_song.started_at is None: + start_time = datetime.datetime.now().timestamp() + else: + start_time = first_song.started_at + + start_time = state.queue.fold( + lambda item, time: time + + item.duration + + state.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 report_to is not None: + await sio.emit( + "msg", + { + "msg": f"The song queue ends at {end_time.hour:02d}:" + f"{end_time.minute:02d}." + }, + room=report_to, + ) + return + + state.queue.append(entry) + await send_state(state, room) + + await sio.emit( + "get-meta-info", + entry, + room=clients[room].sid, + ) + + @sio.on("append") async def handle_append(sid: str, data: dict[str, Any]) -> None: """ @@ -209,46 +296,12 @@ async def handle_append(sid: str, data: dict[str, Any]) -> None: source_obj = state.config.sources[data["source"]] entry = await source_obj.get_entry(data["performer"], data["ident"]) if entry is None: - await sio.emit("mst", {"msg": f"Unable to append {data['ident']}"}) + await sio.emit("msg", {"msg": f"Unable to append {data['ident']}"}) return entry.uid = data["uid"] if "uid" in data else None - first_song = state.queue.try_peek() - if first_song is None or first_song.started_at is None: - start_time = datetime.datetime.now().timestamp() - else: - start_time = first_song.started_at - - start_time = state.queue.fold( - lambda item, time: time - + item.duration - + state.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) - await sio.emit( - "msg", - { - "msg": f"The song queue ends at {end_time.hour:02d}:" - f"{end_time.minute:02d}." - }, - room=sid, - ) - return - - state.queue.append(entry) - await send_state(state, room) - - await sio.emit( - "get-meta-info", - entry, - room=clients[room].sid, - ) + await append_to_queue(room, entry, sid) @sio.on("meta-info") @@ -278,6 +331,10 @@ async def handle_meta_info(sid: str, data: dict[str, Any]) -> None: lambda item: item.update(**data["meta"]), ) + for entry in state.waiting_room: + if entry.uuid == data["uuid"] or str(entry.uuid) == data["uuid"]: + entry.update(**data["meta"]) + await send_state(state, room) @@ -310,6 +367,28 @@ async def handle_get_first(sid: str) -> None: await sio.emit("play", current, room=sid) +async def discard_first(room) -> Entry: + state = clients[room] + + old_entry = await state.queue.popleft() + + # append items from the waiting room + first_entry_for_uid = None + for wr_entry in state.waiting_room: + if wr_entry.uid == old_entry.uid: + first_entry_for_uid = wr_entry + break + + if first_entry_for_uid is not None: + await append_to_queue(room, first_entry_for_uid) + state.waiting_room.remove(first_entry_for_uid) + + state.recent.append(old_entry) + state.last_seen = datetime.datetime.now() + + return old_entry + + @sio.on("pop-then-get-next") async def handle_pop_then_get_next(sid: str) -> None: """ @@ -336,11 +415,9 @@ async def handle_pop_then_get_next(sid: str) -> None: if sid != state.sid: return - old_entry = await state.queue.popleft() - state.recent.append(old_entry) - state.last_seen = datetime.datetime.now() - + await discard_first(room) await send_state(state, room) + current = await state.queue.peek() current.started_at = datetime.datetime.now().timestamp() await send_state(state, room) @@ -448,12 +525,17 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None: ) else: logger.info("Registerd new client %s", room) + print(data) initial_entries = [Entry(**entry) for entry in data["queue"]] + initial_waiting_room = [ + Entry(**entry) for entry in data["waiting_room"] + ] initial_recent = [Entry(**entry) for entry in data["recent"]] clients[room] = State( secret=data["secret"], queue=Queue(initial_entries), + waiting_room=initial_waiting_room, recent=initial_recent, sid=sid, config=Config(sources={}, sources_prio=[], **data["config"]), @@ -637,8 +719,7 @@ async def handle_skip_current(sid: str) -> None: state = clients[room] if is_admin: - old_entry = await state.queue.popleft() - state.recent.append(old_entry) + old_entry = await discard_first(room) await sio.emit("skip-current", old_entry, room=clients[room].sid) await send_state(state, room) diff --git a/syng/webclientmockup.py b/syng/webclientmockup.py index 0580cee..4f8d213 100644 --- a/syng/webclientmockup.py +++ b/syng/webclientmockup.py @@ -2,7 +2,7 @@ # pylint: disable=missing-module-docstring # pylint: disable=missing-class-docstring import asyncio -from typing import Any +from typing import Any, Optional from aiocmd import aiocmd import socketio @@ -30,6 +30,12 @@ async def handle_state(data: dict[str, Any]) -> None: print( f"\t{item.performer}: {item.artist} - {item.title} ({item.duration})" ) + print("Waiting Room") + for raw_item in data["shadow_queue"]: + item = Entry(**raw_item) + print( + f"\t{item.performer}: {item.artist} - {item.title} ({item.duration})" + ) print("Recent") for raw_item in data["recent"]: item = Entry(**raw_item) @@ -38,8 +44,13 @@ async def handle_state(data: dict[str, Any]) -> None: ) +@sio.on("msg") +async def handle_msg(data: dict[str, Any]) -> None: + print(data["msg"]) + + @sio.on("connect") -async def handle_connect(_: dict[str, Any]) -> None: +async def handle_connect() -> None: print("Connected") await sio.emit("register-web", {"room": state["room"]}) @@ -64,6 +75,7 @@ class SyngShell(aiocmd.PromptToolkitCmd): { "performer": "Hammy", "source": "youtube", + "uid": "mockup", # https://youtube.com/watch?v=x5bM5Bdizi4", "ident": "https://www.youtube.com/watch?v=rqZqHXJm-UA", }, @@ -72,9 +84,30 @@ class SyngShell(aiocmd.PromptToolkitCmd): async def do_search(self, query: str) -> None: await sio.emit("search", {"query": query}) - async def do_append(self, source: str, ident: str) -> None: + async def do_append( + self, source: str, ident: str, uid: Optional[str] = None + ) -> None: await sio.emit( - "append", {"performer": "Hammy", "source": source, "ident": ident} + "append", + { + "performer": "Mockup", + "source": source, + "ident": ident, + "uid": uid if uid is not None else "mockup", + }, + ) + + async def do_waiting_room( + self, source: str, ident: str, uid: Optional[str] = None + ) -> None: + await sio.emit( + "shadow-append", + { + "performer": "Mockup", + "source": source, + "ident": ident, + "uid": uid if uid is not None else "mockup", + }, ) async def do_admin(self, data: str) -> None: