Docstrings and documentation
This commit is contained in:
parent
37e09d10da
commit
b6ec014914
10 changed files with 442 additions and 61 deletions
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
|
@ -0,0 +1,35 @@
|
|||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=source
|
||||
set BUILDDIR=build
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.https://www.sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
6
docs/source/client.rst
Normal file
6
docs/source/client.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
Client
|
||||
======
|
||||
|
||||
.. automodule:: syng.client
|
||||
:members:
|
||||
|
30
docs/source/conf.py
Normal file
30
docs/source/conf.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath(".."))
|
||||
|
||||
project = "syng"
|
||||
copyright = "2022, Christoph Stahl"
|
||||
author = "Christoph Stahl"
|
||||
release = "2.0.0"
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = ["sphinx.ext.autodoc"]
|
||||
|
||||
templates_path = ["_templates"]
|
||||
exclude_patterns = []
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
html_theme = "alabaster"
|
||||
html_static_path = ["_static"]
|
5
docs/source/entry.rst
Normal file
5
docs/source/entry.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
Entry
|
||||
=====
|
||||
|
||||
.. automodule:: syng.entry
|
||||
:members:
|
18
docs/source/index.rst
Normal file
18
docs/source/index.rst
Normal file
|
@ -0,0 +1,18 @@
|
|||
Welcome to syng's documentation!
|
||||
================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
server
|
||||
client
|
||||
entry
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
6
docs/source/server.rst
Normal file
6
docs/source/server.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
Server
|
||||
======
|
||||
|
||||
.. automodule:: syng.server
|
||||
:members:
|
||||
|
207
syng/client.py
207
syng/client.py
|
@ -1,21 +1,24 @@
|
|||
import asyncio
|
||||
import string
|
||||
import secrets
|
||||
from traceback import print_exc
|
||||
from json import load
|
||||
import logging
|
||||
from argparse import ArgumentParser
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Any
|
||||
import tempfile
|
||||
import datetime
|
||||
import logging
|
||||
import secrets
|
||||
import string
|
||||
import tempfile
|
||||
from argparse import ArgumentParser
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
from json import load
|
||||
from traceback import print_exc
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
|
||||
import socketio
|
||||
import pyqrcode
|
||||
import socketio
|
||||
from PIL import Image
|
||||
|
||||
from .sources import Source, configure_sources
|
||||
from .entry import Entry
|
||||
from .sources import configure_sources
|
||||
from .sources import Source
|
||||
|
||||
|
||||
sio: socketio.AsyncClient = socketio.AsyncClient()
|
||||
|
@ -28,6 +31,33 @@ currentLock: asyncio.Semaphore = asyncio.Semaphore(0)
|
|||
|
||||
@dataclass
|
||||
class State:
|
||||
"""This captures the current state of the playback client.
|
||||
|
||||
It doubles as a backup of the state of the :py:class:`syng.server` in case
|
||||
the server needs to be restarted.
|
||||
|
||||
:param current_source: This holds a reference to the
|
||||
:py:class:`syng.sources.source.Source` object, that is currently
|
||||
playing. If no song is played, the value is `None`.
|
||||
:type current_source: Optional[Source]
|
||||
:param queue: A copy of the current playlist on the server.
|
||||
:type queue: 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.
|
||||
:type room: str
|
||||
:param secret: The passcode of the room. If a playback client reconnects to
|
||||
a room, this must be identical. Also, if a webclient wants to have
|
||||
admin privileges, this must be included.
|
||||
:type secret: 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]
|
||||
"""
|
||||
|
||||
current_source: Optional[Source] = None
|
||||
queue: list[Entry] = field(default_factory=list)
|
||||
recent: list[Entry] = field(default_factory=list)
|
||||
|
@ -38,6 +68,16 @@ class State:
|
|||
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,
|
||||
|
@ -49,6 +89,16 @@ state: State = State()
|
|||
|
||||
@sio.on("skip-current")
|
||||
async def handle_skip_current(_: dict[str, Any] = {}) -> None:
|
||||
"""
|
||||
Handle the "skip-current" message.
|
||||
|
||||
Skips the song, that is currently played. If playback currently waits for
|
||||
buffering, the buffering is also aborted.
|
||||
|
||||
:param _: Data part of the message, ignored
|
||||
:type _: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
logger.info("Skipping current")
|
||||
if state.current_source is not None:
|
||||
await state.current_source.skip_current(state.queue[0])
|
||||
|
@ -56,6 +106,20 @@ async def handle_skip_current(_: dict[str, Any] = {}) -> None:
|
|||
|
||||
@sio.on("state")
|
||||
async def handle_state(data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "state" message.
|
||||
|
||||
The "state" message forwards the current queue and recent list from the
|
||||
server. This function saves a copy of both in the global
|
||||
:py:class:`State`:.
|
||||
|
||||
After recieving the new state, a buffering task for the first elements of
|
||||
the queue is started.
|
||||
|
||||
:param data: A dictionary with the `queue` and `recent` list.
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
state.queue = [Entry(**entry) for entry in data["queue"]]
|
||||
state.recent = [Entry(**entry) for entry in data["recent"]]
|
||||
|
||||
|
@ -66,6 +130,25 @@ async def handle_state(data: dict[str, Any]) -> None:
|
|||
|
||||
@sio.on("connect")
|
||||
async def handle_connect(_: dict[str, Any] = {}) -> None:
|
||||
"""
|
||||
Handle the "connect" message.
|
||||
|
||||
Called when the client successfully connects or reconnects to the server.
|
||||
Sends a `register-client` message to the server with the initial state and
|
||||
configuration of the client, consiting of the currently saved
|
||||
:py:attr:`State.queue` and :py:attr:`State.recent` field of the global
|
||||
:py:class:`State`, as well a room code the client wants to connect to, a
|
||||
secret to secure the access to the room and a config dictionary.
|
||||
|
||||
If the room code is `None`, the server will issue a room code.
|
||||
|
||||
This message will be handled by the
|
||||
:py:func:`syng.server.handle_register_client` function of the server.
|
||||
|
||||
:param _: Data part of the message, ignored
|
||||
:type _: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
logging.info("Connected to server")
|
||||
await sio.emit(
|
||||
"register-client",
|
||||
|
@ -79,14 +162,38 @@ async def handle_connect(_: dict[str, Any] = {}) -> None:
|
|||
)
|
||||
|
||||
|
||||
@sio.on("buffer")
|
||||
async def handle_buffer(data: dict[str, Any]) -> None:
|
||||
@sio.on("get-meta-info")
|
||||
async def handle_get_meta_info(data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle a "get-meta-info" message.
|
||||
|
||||
Collects the metadata from a given :py:class:`Entry`, from its source, and
|
||||
sends them back to the server in a "meta-info" message. On the server side
|
||||
a :py:func:`syng.server.handle_meta_info` function is called.
|
||||
|
||||
:param data: A dictionary encoding the entry
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
source: Source = sources[data["source"]]
|
||||
meta_info: dict[str, Any] = await source.get_missing_metadata(Entry(**data))
|
||||
await sio.emit("meta-info", {"uuid": data["uuid"], "meta": meta_info})
|
||||
|
||||
|
||||
async def preview(entry: Entry) -> None:
|
||||
"""
|
||||
Generate and play a preview for a given :py:class:`Entry`.
|
||||
|
||||
This function shows a black screen and prints the artist, title and
|
||||
performer of the entry for a duration.
|
||||
|
||||
This is done by creating a black png file, and showing subtitles in the
|
||||
middle of the screen.... don't ask, it works
|
||||
|
||||
:param entry: The entry to preview
|
||||
:type entry: :py:class:`Entry`
|
||||
:rtype: None
|
||||
"""
|
||||
background = Image.new("RGB", (1280, 720))
|
||||
subtitle: str = f"""1
|
||||
00:00:00,00 --> 00:05:00,00
|
||||
|
@ -97,7 +204,7 @@ async def preview(entry: Entry) -> None:
|
|||
process = await asyncio.create_subprocess_exec(
|
||||
"mpv",
|
||||
tmpfile.name,
|
||||
"--image-display-duration=3",
|
||||
f"--image-display-duration={state.preview_duration}",
|
||||
"--sub-pos=50",
|
||||
"--sub-file=-",
|
||||
"--fullscreen",
|
||||
|
@ -110,28 +217,62 @@ async def preview(entry: Entry) -> None:
|
|||
|
||||
@sio.on("play")
|
||||
async def handle_play(data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "play" message.
|
||||
|
||||
Plays the :py:class:`Entry`, that is encoded in the `data` parameter. If a
|
||||
:py:attr:`State.preview_duration` is set, it shows a small preview before
|
||||
that.
|
||||
|
||||
:param data: A dictionary encoding the entry
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
entry: Entry = Entry(**data)
|
||||
print(
|
||||
f"Playing: {entry.artist} - {entry.title} [{entry.album}] ({entry.source}) for {entry.performer}"
|
||||
)
|
||||
try:
|
||||
state.current_source = sources[entry.source]
|
||||
if state.preview_duration > 0:
|
||||
await preview(entry)
|
||||
await sources[entry.source].play(entry)
|
||||
except Exception:
|
||||
print_exc()
|
||||
state.current_source = None
|
||||
await sio.emit("pop-then-get-next")
|
||||
|
||||
|
||||
@sio.on("client-registered")
|
||||
async def handle_register(data: dict[str, Any]) -> None:
|
||||
async def handle_client_registered(data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "client-registered" massage.
|
||||
|
||||
If the registration was successfull (`data["success"]` == `True`), store
|
||||
the room code in the global :py:class:`State` and print out a link to join
|
||||
the webclient.
|
||||
|
||||
Start listing all configured :py:class:`syng.sources.source.Source` to the
|
||||
server via a "sources" message. This message will be handled by the
|
||||
:py:func:`server.handle_sources` function and may request additional
|
||||
configuration for each source.
|
||||
|
||||
If there is no song playing, start requesting the first song of the queue
|
||||
with a "get-first" message. This will be handled on the server by the
|
||||
:py:func:`server.handle_get_first` function.
|
||||
|
||||
:param data: A dictionary containing a `success` and a `room` entry.
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
if data["success"]:
|
||||
logging.info("Registered")
|
||||
print(f"Join here: {state.server}/{data['room']}")
|
||||
print(pyqrcode.create(f"{state.server}/{data['room']}").terminal(quiet_zone=1))
|
||||
print(pyqrcode.create(
|
||||
f"{state.server}/{data['room']}").terminal(quiet_zone=1))
|
||||
state.room = data["room"]
|
||||
await sio.emit("sources", {"sources": list(sources.keys())})
|
||||
if state.current_source is None:
|
||||
if state.current_source is None: # A possible race condition can occur here
|
||||
await sio.emit("get-first")
|
||||
else:
|
||||
logging.warning("Registration failed")
|
||||
|
@ -140,6 +281,21 @@ async def handle_register(data: dict[str, Any]) -> None:
|
|||
|
||||
@sio.on("request-config")
|
||||
async def handle_request_config(data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "request-config" message.
|
||||
|
||||
Sends the specific server side configuration for a given
|
||||
:py:class:`syng.sources.source.Source`.
|
||||
|
||||
A Source can decide, that the config will be split up in multiple Parts.
|
||||
If this is the case, multiple "config-chunk" messages will be send with a
|
||||
running enumerator. Otherwise a singe "config" message will be send.
|
||||
|
||||
:param data: A dictionary with the entry `source` and a string, that
|
||||
corresponds to the name of a source.
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
if data["source"] in sources:
|
||||
config: dict[str, Any] | list[dict[str, Any]] = await sources[
|
||||
data["source"]
|
||||
|
@ -161,6 +317,17 @@ async def handle_request_config(data: dict[str, Any]) -> None:
|
|||
|
||||
|
||||
async def aiomain() -> None:
|
||||
"""
|
||||
Async main function.
|
||||
|
||||
Parses the arguments, reads a config file and sets default values. Then
|
||||
connects to a specified server.
|
||||
|
||||
If no secret is given, a random secret will be generated and presented to
|
||||
the user.
|
||||
|
||||
:rtype: None
|
||||
"""
|
||||
parser: ArgumentParser = ArgumentParser()
|
||||
|
||||
parser.add_argument("--room", "-r")
|
||||
|
@ -199,9 +366,5 @@ async def aiomain() -> None:
|
|||
await sio.wait()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
asyncio.run(aiomain())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
asyncio.run(aiomain())
|
||||
|
|
172
syng/server.py
172
syng/server.py
|
@ -1,58 +1,93 @@
|
|||
from __future__ import annotations
|
||||
from collections import deque
|
||||
from typing import Any, Callable, Optional
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
import string
|
||||
import random
|
||||
import logging
|
||||
from argparse import ArgumentParser
|
||||
from uuid import UUID
|
||||
import datetime
|
||||
|
||||
from aiohttp import web
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
from argparse import ArgumentParser
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
import socketio
|
||||
from aiohttp import web
|
||||
|
||||
from .entry import Entry
|
||||
from .sources import Source, available_sources
|
||||
from .sources import available_sources
|
||||
from .sources import Source
|
||||
|
||||
sio = socketio.AsyncServer(cors_allowed_origins="*", logger=True, engineio_logger=False)
|
||||
sio = socketio.AsyncServer(cors_allowed_origins="*",
|
||||
logger=True, engineio_logger=False)
|
||||
app = web.Application()
|
||||
sio.attach(app)
|
||||
|
||||
|
||||
async def root_handler(request: Any) -> Any:
|
||||
"""
|
||||
Handle the index and favicon requests.
|
||||
|
||||
If the path of the request ends with "/favicon.ico" return the favicon,
|
||||
otherwise the index.html. This way the javascript can read the room code
|
||||
from the url.
|
||||
|
||||
:param request Any: Webrequest from aiohttp
|
||||
:return: Either the favicon or the index.html
|
||||
:rtype web.FileResponse:
|
||||
"""
|
||||
if request.path.endswith("/favicon.ico"):
|
||||
return web.FileResponse("syng/static/favicon.ico")
|
||||
return web.FileResponse("syng/static/index.html")
|
||||
|
||||
|
||||
async def favico_handler(_: Any) -> Any:
|
||||
return web.FileResponse("syng/static/favicon.ico")
|
||||
|
||||
|
||||
app.add_routes([web.static("/assets/", "syng/static/assets/")])
|
||||
app.router.add_route("*", "/", root_handler)
|
||||
app.router.add_route("*", "/{room}", root_handler)
|
||||
app.router.add_route("*", "/{room}/", root_handler)
|
||||
app.router.add_route("*", "/favicon.ico", favico_handler)
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Queue:
|
||||
"""A async queue with synchronization.
|
||||
|
||||
This queue keeps track of the amount of entries by using a semaphore.
|
||||
|
||||
:param initial_entries: Initial list of entries to add to the queue
|
||||
:type initial_entries: list[Entry]
|
||||
"""
|
||||
|
||||
def __init__(self, initial_entries: list[Entry]):
|
||||
"""
|
||||
Construct the queue. And initialize the internal lock and semaphore.
|
||||
|
||||
:param initial_entries: Initial list of entries to add to the queue
|
||||
:type initial_entries: list[Entry]
|
||||
"""
|
||||
self._queue = deque(initial_entries)
|
||||
|
||||
self.num_of_entries_sem = asyncio.Semaphore(len(self._queue))
|
||||
self.readlock = asyncio.Lock()
|
||||
|
||||
def append(self, x: Entry) -> None:
|
||||
self._queue.append(x)
|
||||
def append(self, entry: Entry) -> None:
|
||||
"""
|
||||
Append an entry to the queue, increase the semaphore.
|
||||
|
||||
:param entry: The entry to add
|
||||
:type entry: Entry
|
||||
:rtype: None
|
||||
"""
|
||||
self._queue.append(entry)
|
||||
self.num_of_entries_sem.release()
|
||||
|
||||
async def peek(self) -> Entry:
|
||||
"""
|
||||
Return the first entry in the queue.
|
||||
|
||||
If the queue is empty, wait until the queue has at least one entry.
|
||||
|
||||
:returns: First entry of the queue
|
||||
:rtype: Entry
|
||||
"""
|
||||
async with self.readlock:
|
||||
await self.num_of_entries_sem.acquire()
|
||||
item = self._queue[0]
|
||||
|
@ -60,33 +95,83 @@ class Queue:
|
|||
return item
|
||||
|
||||
async def popleft(self) -> Entry:
|
||||
"""
|
||||
Remove the first entry in the queue and return it.
|
||||
|
||||
Decreases the semaphore. If the queue is empty, wait until the queue
|
||||
has at least one entry.
|
||||
|
||||
:returns: First entry of the queue
|
||||
:rtype: Entry
|
||||
"""
|
||||
async with self.readlock:
|
||||
await self.num_of_entries_sem.acquire()
|
||||
item = self._queue.popleft()
|
||||
return item
|
||||
|
||||
def to_dict(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Forward the to_dict request to all entries and return it in a list.
|
||||
|
||||
This is done, so that the entries can be converted to a JSON object,
|
||||
when sending it to the web or playback client.
|
||||
|
||||
:returns: A list with dictionaries, that encode the enties in the
|
||||
queue.
|
||||
:rtype: list[dict[str, Any]]
|
||||
"""
|
||||
return [item.to_dict() for item in self._queue]
|
||||
|
||||
def update(
|
||||
self, locator: Callable[[Entry], Any], updater: Callable[[Entry], None]
|
||||
) -> None:
|
||||
def update(self, uuid: UUID | str, updater: Callable[[Entry], None]) -> None:
|
||||
"""
|
||||
Update entries in the queue, identified by their uuid.
|
||||
|
||||
:param uuid: The uuid of the entry to update
|
||||
:type uuid: UUID | str
|
||||
:param updater: A function, that updates the entry
|
||||
:type updater: Callable[[Entry], None]
|
||||
:rtype: None
|
||||
"""
|
||||
for item in self._queue:
|
||||
if locator(item):
|
||||
if item.uuid == uuid or str(item.uuid) == uuid:
|
||||
updater(item)
|
||||
|
||||
def find_by_uuid(self, uuid: UUID | str) -> Optional[Entry]:
|
||||
"""
|
||||
Find an entry by its uuid and return it.
|
||||
|
||||
:param uuid: The uuid to search for.
|
||||
:type uuid: UUID | str
|
||||
:returns: The entry with the uuid or `None` if no such entry exists
|
||||
:rtype: Optional[Entry]
|
||||
"""
|
||||
for item in self._queue:
|
||||
if item.uuid == uuid or str(item.uuid) == uuid:
|
||||
return item
|
||||
return None
|
||||
|
||||
async def remove(self, entry: Entry) -> None:
|
||||
"""
|
||||
Remove an entry, if it exists. Decrease the semaphore.
|
||||
|
||||
:param entry: The entry to remove
|
||||
:type entry: Entry
|
||||
:rtype: None
|
||||
"""
|
||||
async with self.readlock:
|
||||
await self.num_of_entries_sem.acquire()
|
||||
self._queue.remove(entry)
|
||||
|
||||
async def moveUp(self, uuid: str) -> None:
|
||||
async def move_up(self, uuid: str) -> None:
|
||||
"""
|
||||
Move an :py:class:`syng.entry.Entry` with the uuid up in the queue.
|
||||
|
||||
If it is called on the first two elements, nothing will happen.
|
||||
|
||||
:param uuid: The uuid of the entry.
|
||||
:type uuid: str
|
||||
:rtype: None
|
||||
"""
|
||||
async with self.readlock:
|
||||
uuid_idx = 0
|
||||
for idx, item in enumerate(self._queue):
|
||||
|
@ -176,7 +261,7 @@ async def handle_append(sid: str, data: dict[str, Any]) -> None:
|
|||
await send_state(state, room)
|
||||
|
||||
await sio.emit(
|
||||
"buffer",
|
||||
"get-meta-info",
|
||||
entry.to_dict(),
|
||||
room=clients[room].sid,
|
||||
)
|
||||
|
@ -189,7 +274,7 @@ async def handle_meta_info(sid: str, data: dict[str, Any]) -> None:
|
|||
state = clients[room]
|
||||
|
||||
state.queue.update(
|
||||
lambda item: str(item.uuid) == data["uuid"],
|
||||
data["uuid"],
|
||||
lambda item: item.update(**data["meta"]),
|
||||
)
|
||||
|
||||
|
@ -226,7 +311,8 @@ async def handle_pop_then_get_next(sid: str, data: dict[str, Any] = {}) -> None:
|
|||
|
||||
|
||||
def gen_id(length: int = 4) -> str:
|
||||
client_id = "".join([random.choice(string.ascii_letters) for _ in range(length)])
|
||||
client_id = "".join([random.choice(string.ascii_letters)
|
||||
for _ in range(length)])
|
||||
if client_id in clients:
|
||||
client_id = gen_id(length + 1)
|
||||
return client_id
|
||||
|
@ -234,6 +320,13 @@ def gen_id(length: int = 4) -> str:
|
|||
|
||||
@sio.on("register-client")
|
||||
async def handle_register_client(sid: str, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
[TODO:description]
|
||||
|
||||
:param sid str: [TODO:description]
|
||||
:param data dict[str, Any]: [TODO:description]
|
||||
:rtype None: [TODO:description]
|
||||
"""
|
||||
room: str = data["room"] if "room" in data and data["room"] else gen_id()
|
||||
async with sio.session(sid) as session:
|
||||
session["room"] = room
|
||||
|
@ -375,14 +468,14 @@ async def handle_skip_current(sid: str, data: dict[str, Any] = {}) -> None:
|
|||
await sio.emit("skip-current", room=clients[room].sid)
|
||||
|
||||
|
||||
@sio.on("moveUp")
|
||||
async def handle_moveUp(sid: str, data: dict[str, Any]) -> None:
|
||||
@sio.on("move-up")
|
||||
async def handle_move_up(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:
|
||||
await state.queue.moveUp(data["uuid"])
|
||||
await state.queue.move_up(data["uuid"])
|
||||
await send_state(state, room)
|
||||
|
||||
|
||||
|
@ -414,7 +507,6 @@ async def handle_search(sid: str, data: dict[str, str]) -> None:
|
|||
state = clients[room]
|
||||
|
||||
query = data["query"]
|
||||
result_futures = []
|
||||
results_list = await asyncio.gather(
|
||||
*[
|
||||
state.config.sources[source].search(query)
|
||||
|
@ -444,6 +536,12 @@ def main() -> None:
|
|||
parser.add_argument("--host", "-H", default="localhost")
|
||||
parser.add_argument("--port", "-p", default="8080")
|
||||
args = parser.parse_args()
|
||||
|
||||
app.add_routes([web.static("/assets/", "syng/static/assets/")])
|
||||
app.router.add_route("*", "/", root_handler)
|
||||
app.router.add_route("*", "/{room}", root_handler)
|
||||
app.router.add_route("*", "/{room}/", root_handler)
|
||||
|
||||
web.run_app(app, host=args.host, port=args.port)
|
||||
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ class YoutubeSource(Source):
|
|||
]
|
||||
|
||||
def _yt_search(self, query: str) -> list[YouTube]:
|
||||
results = Search(f"{query} karaoke").results
|
||||
results: Optional[list[YouTube]] = Search(f"{query} karaoke").results
|
||||
if results is not None:
|
||||
return results
|
||||
return []
|
||||
|
|
Loading…
Add table
Reference in a new issue