Preparation for 2.0 release

This commit is contained in:
Christoph Stahl 2023-12-18 18:56:03 +01:00
parent 65eb9bd7bf
commit 14821ab759
10 changed files with 231 additions and 212 deletions

66
customtkinter.pyi Normal file
View file

@ -0,0 +1,66 @@
from tkinter import Tk
from typing import Any, Callable, Optional
from PIL.Image import Image
class CTk(Tk):
def __init__(self, parent: Optional[Tk] = None, className: str = "Tk") -> None:
pass
def pack(
self,
expand: bool = False,
fill: str = "",
side: str = "",
padx: int = 0,
pady: int = 0,
ipadx: int = 0,
ipady: int = 0,
anchor: str = "",
) -> None: ...
def grid(
self, column: int, row: int, padx: int = 0, pady: int = 0, sticky: str = ""
) -> None: ...
def configure(self, **kwargs: Any) -> None: ...
class CTkToplevel(CTk): ...
class CTkFrame(CTk): ...
class CTkImage:
def __init__(self, light_image: Image, size: tuple[int, int]) -> None: ...
class CTkTabview(CTk):
def __init__(self, parent: Tk, width: int, height: int) -> None: ...
def add(self, name: str) -> None: ...
def set(self, name: str) -> None: ...
def tab(self, name: str) -> CTkFrame: ...
class CTkOptionMenu(CTk):
def __init__(self, parent: Tk, values: list[str]) -> None: ...
def set(self, value: str) -> None: ...
def get(self) -> str: ...
class CTkCheckBox(CTk):
def __init__(self, parent: Tk, text: str, onvalue: Any, offvalue: Any) -> None: ...
def select(self) -> None: ...
def deselect(self) -> None: ...
def get(self) -> Any: ...
class CTkLabel(CTk):
def __init__(self, parent: Tk, text: str, justify: str = "") -> None: ...
class CTkTextbox(CTk):
def __init__(self, parent: Tk, wrap: str = "none", height: int = 1) -> None: ...
def get(self, start: str, end: str) -> str: ...
def delete(self, start: str, end: str) -> None: ...
def insert(self, start: str, value: str) -> None: ...
class CTkScrollableFrame(CTk): ...
class CTkButton(CTk):
def __init__(
self,
parent: Tk,
text: str,
command: Callable[..., None],
width: Optional[int] = None,
) -> None: ...

View file

@ -6,6 +6,7 @@
# 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 os
import sys import sys
import sphinx_rtd_theme
sys.path.insert(0, os.path.abspath("..")) sys.path.insert(0, os.path.abspath(".."))
@ -17,7 +18,7 @@ release = "2.0.0"
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = ["sphinx.ext.autodoc"] extensions = ["sphinx.ext.autodoc", "sphinx_rtd_theme"]
templates_path = ["_templates"] templates_path = ["_templates"]
exclude_patterns = [] exclude_patterns = []
@ -26,5 +27,5 @@ exclude_patterns = []
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "alabaster" html_theme = "sphinx_rtd_theme"
html_static_path = ["_static"] html_static_path = ["_static"]

View file

@ -30,6 +30,9 @@ pyyaml = "^6.0.1"
async-tkinter-loop = "^0.9.2" async-tkinter-loop = "^0.9.2"
tkcalendar = "^1.6.1" tkcalendar = "^1.6.1"
tktimepicker = "^2.0.2" tktimepicker = "^2.0.2"
types-pyyaml = "^6.0.12.12"
types-pillow = "^10.1.0.2"
platformdirs = "^4.0.0"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
@ -40,17 +43,20 @@ exclude = [ ".venv" ]
venvPath = "." venvPath = "."
venv = ".venv" venv = ".venv"
[tool.pylint."MESSAGES CONTROL"]
disable = '''too-many-lines,
too-many-ancestors
'''
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = [ module = [
"aiohttp",
"pytube",
"minio",
"aiocmd",
"pyqrcodeng",
"socketio",
"pillow",
"PIL",
"yt_dlp", "yt_dlp",
"pymediainfo",
"minio",
"qrcode",
"engineio",
"tkcalendar",
"tktimepicker"
] ]
ignore_missing_imports = true ignore_missing_imports = true

View file

@ -16,19 +16,16 @@ Excerp from the help::
--config-file CONFIG_FILE, -C CONFIG_FILE --config-file CONFIG_FILE, -C CONFIG_FILE
--key KEY, -k KEY --key KEY, -k KEY
The config file should be a json file in the following style:: The config file should be a yaml file in the following style::
{ sources:
"sources": { SOURCE1:
"SOURCE1": { configuration for SOURCE }, configuration for SOURCE
"SOURCE2": { configuration for SOURCE }, SOURCE2:
configuration for SOURCE
... ...
}, config:
},
"config": {
configuration for the client configuration for the client
}
}
""" """
import asyncio import asyncio
import datetime import datetime
@ -40,21 +37,19 @@ import signal
from argparse import ArgumentParser from argparse import ArgumentParser
from dataclasses import dataclass from dataclasses import dataclass
from dataclasses import field from dataclasses import field
from yaml import load, Loader
from traceback import print_exc from traceback import print_exc
from typing import Any from typing import Any, Optional
from typing import Optional
import qrcode import qrcode
import socketio import socketio
import engineio import engineio
from PIL import Image from PIL import Image
from yaml import load, Loader
from . import jsonencoder from . import jsonencoder
from .entry import Entry from .entry import Entry
from .sources import configure_sources from .sources import configure_sources, Source
from .sources import Source
sio: socketio.AsyncClient = socketio.AsyncClient(json=jsonencoder) sio: socketio.AsyncClient = socketio.AsyncClient(json=jsonencoder)
@ -106,7 +101,7 @@ class State:
* `last_song` (`Optional[datetime.datetime]`): A timestamp, defining the end of * `last_song` (`Optional[datetime.datetime]`): A timestamp, defining the end of
the queue. the queue.
* `waiting_room_policy` (Optional[str]): One of: * `waiting_room_policy` (Optional[str]): One of:
- `force`, if a performer is already in the queue, they are put in the - `forced`, if a performer is already in the queue, they are put in the
waiting room. waiting room.
- `optional`, if a performer is already in the queue, they have the option - `optional`, if a performer is already in the queue, they have the option
to be put in the waiting room. to be put in the waiting room.
@ -200,12 +195,8 @@ async def handle_connect() -> None:
"queue": state.queue, "queue": state.queue,
"waiting_room": state.waiting_room, "waiting_room": state.waiting_room,
"recent": state.recent, "recent": state.recent,
# "room": state.config["room"],
# "secret": state.config["secret"],
"config": state.config, "config": state.config,
} }
if state.config["key"]:
data["registration-key"] = state.config["key"] # TODO: unify
await sio.emit("register-client", data) await sio.emit("register-client", data)
@ -357,9 +348,7 @@ async def handle_request_config(data: dict[str, Any]) -> None:
:rtype: None :rtype: None
""" """
if data["source"] in sources: if data["source"] in sources:
config: dict[str, Any] | list[dict[str, Any]] = await sources[ config: dict[str, Any] | list[dict[str, Any]] = await sources[data["source"]].get_config()
data["source"]
].get_config()
if isinstance(config, list): if isinstance(config, list):
num_chunks: int = len(config) num_chunks: int = len(config)
for current, chunk in enumerate(config): for current, chunk in enumerate(config):
@ -376,7 +365,7 @@ async def handle_request_config(data: dict[str, Any]) -> None:
await sio.emit("config", {"source": data["source"], "config": config}) await sio.emit("config", {"source": data["source"], "config": config})
def signal_handler(): def signal_handler() -> None:
engineio.async_client.async_signal_handler() engineio.async_client.async_signal_handler()
if state.current_source is not None: if state.current_source is not None:
if state.current_source.player is not None: if state.current_source.player is not None:
@ -426,7 +415,7 @@ async def start_client(config: dict[str, Any]) -> None:
state.current_source.player.kill() state.current_source.player.kill()
def create_async_and_start_client(config): def create_async_and_start_client(config: dict[str, Any]) -> None:
asyncio.run(start_client(config)) asyncio.run(start_client(config))

View file

@ -1,4 +1,4 @@
import asyncio from multiprocessing import Process
from collections.abc import Callable from collections.abc import Callable
from datetime import datetime, date, time from datetime import datetime, date, time
import os import os
@ -6,19 +6,19 @@ import builtins
from functools import partial from functools import partial
from typing import Any, Optional from typing import Any, Optional
import webbrowser import webbrowser
import PIL
from yaml import load, Loader
import multiprocessing import multiprocessing
import customtkinter
import qrcode
import secrets import secrets
import string import string
from tkinter import PhotoImage, Tk, filedialog
from tkcalendar import Calendar
from tktimepicker import SpinTimePickerOld, AnalogPicker, AnalogThemes
from tktimepicker import constants
from .client import create_async_and_start_client, default_config, start_client from PIL import ImageTk
from yaml import dump, load, Loader, Dumper
import customtkinter
from qrcode import QRCode
from tkcalendar import Calendar
from tktimepicker import AnalogPicker, AnalogThemes, constants
import platformdirs
from .client import create_async_and_start_client, default_config
from .sources import available_sources from .sources import available_sources
from .server import main as server_main from .server import main as server_main
@ -57,9 +57,7 @@ class DateAndTimePickerWindow(customtkinter.CTkToplevel):
self.timepicker.pack(expand=True, fill="both") self.timepicker.pack(expand=True, fill="both")
button = customtkinter.CTkButton( button = customtkinter.CTkButton(self, text="Ok", command=partial(self.insert, input_field))
self, text="Ok", command=partial(self.insert, input_field)
)
button.pack(expand=True, fill="x") button.pack(expand=True, fill="x")
def insert(self, input_field: customtkinter.CTkTextbox) -> None: def insert(self, input_field: customtkinter.CTkTextbox) -> None:
@ -105,17 +103,13 @@ class OptionFrame(customtkinter.CTkScrollableFrame):
description: str, description: str,
value: str = "", value: str = "",
callback: Optional[Callable[..., None]] = None, callback: Optional[Callable[..., None]] = None,
): ) -> None:
self.add_option_label(description) self.add_option_label(description)
if value is None: if value is None:
value = "" value = ""
self.string_options[name] = customtkinter.CTkTextbox( self.string_options[name] = customtkinter.CTkTextbox(self, wrap="none", height=1)
self, wrap="none", height=1 self.string_options[name].grid(column=1, row=self.number_of_options, sticky="EW")
)
self.string_options[name].grid(
column=1, row=self.number_of_options, sticky="EW"
)
self.string_options[name].insert("0.0", value) self.string_options[name].insert("0.0", value)
if callback is not None: if callback is not None:
self.string_options[name].bind("<KeyRelease>", callback) self.string_options[name].bind("<KeyRelease>", callback)
@ -160,7 +154,7 @@ class OptionFrame(customtkinter.CTkScrollableFrame):
self, self,
name: str, name: str,
description: str, description: str,
value: list[str] = [], value: list[str],
callback: Optional[Callable[..., None]] = None, callback: Optional[Callable[..., None]] = None,
) -> None: ) -> None:
self.add_option_label(description) self.add_option_label(description)
@ -185,32 +179,26 @@ class OptionFrame(customtkinter.CTkScrollableFrame):
) -> None: ) -> None:
self.add_option_label(description) self.add_option_label(description)
self.choose_options[name] = customtkinter.CTkOptionMenu(self, values=values) self.choose_options[name] = customtkinter.CTkOptionMenu(self, values=values)
self.choose_options[name].grid( self.choose_options[name].grid(column=1, row=self.number_of_options, sticky="EW")
column=1, row=self.number_of_options, sticky="EW"
)
self.choose_options[name].set(value) self.choose_options[name].set(value)
self.number_of_options += 1 self.number_of_options += 1
def open_date_and_time_picker( def open_date_and_time_picker(self, name: str, input_field: customtkinter.CTkTextbox) -> None:
self, name: str, input_field: customtkinter.CTkTextbox
) -> None:
if ( if (
name not in self.date_and_time_pickers name not in self.date_and_time_pickers
or not self.date_and_time_pickers[name].winfo_exists() or not self.date_and_time_pickers[name].winfo_exists()
): ):
self.date_and_time_pickers[name] = DateAndTimePickerWindow( self.date_and_time_pickers[name] = DateAndTimePickerWindow(self, input_field)
self, input_field
)
else: else:
self.date_and_time_pickers[name].focus() self.date_and_time_pickers[name].focus()
def add_date_time_option(self, name: str, description: str, value: str) -> None: def add_date_time_option(self, name: str, description: str, value: str) -> None:
self.add_option_label(description) self.add_option_label(description)
self.date_time_options[name] = None
input_and_button = customtkinter.CTkFrame(self) input_and_button = customtkinter.CTkFrame(self)
input_and_button.grid(column=1, row=self.number_of_options, sticky="EW") input_and_button.grid(column=1, row=self.number_of_options, sticky="EW")
input_field = customtkinter.CTkTextbox(input_and_button, wrap="none", height=1) input_field = customtkinter.CTkTextbox(input_and_button, wrap="none", height=1)
input_field.pack(side="left", fill="x", expand=True) input_field.pack(side="left", fill="x", expand=True)
self.date_time_options[name] = input_field
try: try:
datetime.fromisoformat(value) datetime.fromisoformat(value)
except TypeError: except TypeError:
@ -229,16 +217,16 @@ class OptionFrame(customtkinter.CTkScrollableFrame):
def __init__(self, parent: customtkinter.CTkFrame) -> None: def __init__(self, parent: customtkinter.CTkFrame) -> None:
super().__init__(parent) super().__init__(parent)
self.columnconfigure((1,), weight=1) self.columnconfigure((1,), weight=1)
self.number_of_options = 0 self.number_of_options: int = 0
self.string_options = {} self.string_options: dict[str, customtkinter.CTkTextbox] = {}
self.choose_options = {} self.choose_options: dict[str, customtkinter.CTkOptionMenu] = {}
self.bool_options = {} self.bool_options: dict[str, customtkinter.CTkCheckBox] = {}
self.list_options = {} self.list_options: dict[str, list[customtkinter.CTkTextbox]] = {}
self.date_time_options = {} self.date_time_options: dict[str, customtkinter.CTkTextbox] = {}
self.date_and_time_pickers = {} self.date_and_time_pickers: dict[str, DateAndTimePickerWindow] = {}
def get_config(self) -> dict[str, Any]: def get_config(self) -> dict[str, Any]:
config = {} config: dict[str, Any] = {}
for name, textbox in self.string_options.items(): for name, textbox in self.string_options.items():
config[name] = textbox.get("0.0", "end").strip() config[name] = textbox.get("0.0", "end").strip()
@ -253,6 +241,9 @@ class OptionFrame(customtkinter.CTkScrollableFrame):
for textbox in textboxes: for textbox in textboxes:
config[name].append(textbox.get("0.0", "end").strip()) config[name].append(textbox.get("0.0", "end").strip())
for name, picker in self.date_time_options.items():
config[name] = picker.get("0.0", "end").strip()
return config return config
@ -293,9 +284,7 @@ class GeneralConfig(OptionFrame):
str(config["waiting_room_policy"]).lower(), str(config["waiting_room_policy"]).lower(),
) )
self.add_date_time_option("last_song", "Time of last song", config["last_song"]) self.add_date_time_option("last_song", "Time of last song", config["last_song"])
self.add_string_option( self.add_string_option("preview_duration", "Preview Duration", config["preview_duration"])
"preview_duration", "Preview Duration", config["preview_duration"]
)
def get_config(self) -> dict[str, Any]: def get_config(self) -> dict[str, Any]:
config = super().get_config() config = super().get_config()
@ -308,15 +297,12 @@ class GeneralConfig(OptionFrame):
class SyngGui(customtkinter.CTk): class SyngGui(customtkinter.CTk):
def loadConfig(self) -> None:
filedialog.askopenfilename()
def on_close(self) -> None: def on_close(self) -> None:
if self.server is not None: if self.syng_server is not None:
self.server.kill() self.syng_server.kill()
if self.client is not None: if self.syng_client is not None:
self.client.kill() self.syng_client.kill()
self.withdraw() self.withdraw()
self.destroy() self.destroy()
@ -326,21 +312,30 @@ class SyngGui(customtkinter.CTk):
self.protocol("WM_DELETE_WINDOW", self.on_close) self.protocol("WM_DELETE_WINDOW", self.on_close)
rel_path = os.path.dirname(__file__) rel_path = os.path.dirname(__file__)
img = PIL.ImageTk.PhotoImage(file=os.path.join(rel_path, "static/syng.png")) img = ImageTk.PhotoImage(file=os.path.join(rel_path, "static/syng.png"))
self.wm_iconbitmap() self.wm_iconbitmap()
self.iconphoto(False, img) self.iconphoto(False, img)
self.server = None self.syng_server: Optional[Process] = None
self.client = None self.syng_client: Optional[Process] = None
self.configfile = os.path.join(platformdirs.user_config_dir("syng"), "config.yaml")
try: try:
with open("syng-client.yaml") as cfile: with open(self.configfile, encoding="utf8") as cfile:
loaded_config = load(cfile, Loader=Loader) loaded_config = load(cfile, Loader=Loader)
except FileNotFoundError: except FileNotFoundError:
print("No config found, using default values")
loaded_config = {} loaded_config = {}
config = {"sources": {}, "config": default_config()} config: dict[str, dict[str, Any]] = {"sources": {}, "config": default_config()}
if "config" in loaded_config:
try:
config["config"] |= loaded_config["config"] config["config"] |= loaded_config["config"]
except (KeyError, TypeError):
print("Could not load config")
# if "config" in loaded_config:
# config["config"] |= loaded_config["config"]
if not config["config"]["secret"]: if not config["config"]["secret"]:
config["config"]["secret"] = "".join( config["config"]["secret"] = "".join(
@ -350,30 +345,26 @@ class SyngGui(customtkinter.CTk):
self.wm_title("Syng") self.wm_title("Syng")
# Buttons # Buttons
fileframe = customtkinter.CTkFrame(self) button_line = customtkinter.CTkFrame(self)
fileframe.pack(side="bottom") button_line.pack(side="bottom", fill="x")
loadbutton = customtkinter.CTkButton( startsyng_serverbutton = customtkinter.CTkButton(
fileframe, button_line, text="Start Local Server", command=self.start_syng_server
text="load",
command=self.loadConfig,
) )
loadbutton.pack(side="left") startsyng_serverbutton.pack(side="left", expand=True, anchor="w", padx=10, pady=5)
self.startbutton = customtkinter.CTkButton( savebutton = customtkinter.CTkButton(button_line, text="Save", command=self.save_config)
fileframe, text="Start", command=self.start_client savebutton.pack(side="left", padx=10, pady=5)
)
self.startbutton.pack(side="right")
# startserverbutton = customtkinter.CTkButton(
# fileframe, text="Start Server", command=self.start_server
# )
# startserverbutton.pack(side="right")
open_web_button = customtkinter.CTkButton( open_web_button = customtkinter.CTkButton(
fileframe, text="Open Web", command=self.open_web button_line, text="Open Web", command=self.open_web
) )
open_web_button.pack(side="left") open_web_button.pack(side="left", pady=5)
self.startbutton = customtkinter.CTkButton(
button_line, text="Save and Start", command=self.start_syng_client
)
self.startbutton.pack(side="left", padx=10, pady=10)
# Tabs and QR Code # Tabs and QR Code
frm = customtkinter.CTkFrame(self) frm = customtkinter.CTkFrame(self)
@ -391,7 +382,7 @@ class SyngGui(customtkinter.CTk):
self.qrlabel.pack(side="left", anchor="n", padx=10, pady=10) self.qrlabel.pack(side="left", anchor="n", padx=10, pady=10)
self.general_config = GeneralConfig( self.general_config = GeneralConfig(
tabview.tab("General"), config["config"], self.updateQr tabview.tab("General"), config["config"], self.update_qr
) )
self.general_config.pack(ipadx=10, fill="both", expand=True) self.general_config.pack(ipadx=10, fill="both", expand=True)
@ -400,62 +391,67 @@ class SyngGui(customtkinter.CTk):
for source_name in available_sources: for source_name in available_sources:
try: try:
source_config = loaded_config["sources"][source_name] source_config = loaded_config["sources"][source_name]
except KeyError: except (KeyError, TypeError):
source_config = {} source_config = {}
self.tabs[source_name] = SourceTab( self.tabs[source_name] = SourceTab(tabview.tab(source_name), source_name, source_config)
tabview.tab(source_name), source_name, source_config
)
self.tabs[source_name].pack(ipadx=10, expand=True, fill="both") self.tabs[source_name].pack(ipadx=10, expand=True, fill="both")
self.updateQr() self.update_qr()
def start_client(self) -> None: def save_config(self) -> None:
if self.client is None: with open(self.configfile, "w", encoding="utf-8") as f:
sources = {} dump(self.gather_config(), f, Dumper=Dumper)
for source, tab in self.tabs.items():
sources[source] = tab.get_config()
general_config = self.general_config.get_config() def gather_config(self) -> dict[str, Any]:
sources = {}
for source, tab in self.tabs.items():
sources[source] = tab.get_config()
config = {"sources": sources, "config": general_config} general_config = self.general_config.get_config()
self.client = multiprocessing.Process(
return {"sources": sources, "config": general_config}
def start_syng_client(self) -> None:
if self.syng_client is None:
config = self.gather_config()
self.syng_client = multiprocessing.Process(
target=create_async_and_start_client, args=(config,) target=create_async_and_start_client, args=(config,)
) )
self.client.start() self.syng_client.start()
self.startbutton.configure(text="Stop") self.startbutton.configure(text="Stop")
else: else:
self.client.terminate() self.syng_client.terminate()
self.client = None self.syng_client = None
self.startbutton.configure(text="Start") self.startbutton.configure(text="Save and Start")
def start_server(self) -> None: def start_syng_server(self) -> None:
self.server = multiprocessing.Process(target=server_main) self.syng_server = multiprocessing.Process(target=server_main)
self.server.start() self.syng_server.start()
def open_web(self) -> None: def open_web(self) -> None:
config = self.general_config.get_config() config = self.general_config.get_config()
server = config["server"] syng_server = config["server"]
server += "" if server.endswith("/") else "/" syng_server += "" if syng_server.endswith("/") else "/"
room = config["room"] room = config["room"]
webbrowser.open(server + room) webbrowser.open(syng_server + room)
def changeQr(self, data: str) -> None: def change_qr(self, data: str) -> None:
qr = qrcode.QRCode(box_size=20, border=2) qr = QRCode(box_size=20, border=2)
qr.add_data(data) qr.add_data(data)
qr.make() qr.make()
qr.print_ascii() qr.print_ascii()
image = qr.make_image().convert("RGB") image = qr.make_image().convert("RGB")
tkQrcode = customtkinter.CTkImage(light_image=image, size=(280, 280)) tk_qrcode = customtkinter.CTkImage(light_image=image, size=(280, 280))
self.qrlabel.configure(image=tkQrcode) self.qrlabel.configure(image=tk_qrcode)
def updateQr(self, _evt: None = None) -> None: def update_qr(self, _evt: None = None) -> None:
config = self.general_config.get_config() config = self.general_config.get_config()
server = config["server"] syng_server = config["server"]
server += "" if server.endswith("/") else "/" syng_server += "" if syng_server.endswith("/") else "/"
room = config["room"] room = config["room"]
print(server + room) print(syng_server + room)
self.changeQr(server + room) self.change_qr(syng_server + room)
def main() -> None: def main() -> None:

0
syng/py.typed Normal file
View file

View file

@ -90,7 +90,7 @@ class Client:
in the calculation of the ETA for songs later in the queue. in the calculation of the ETA for songs later in the queue.
* `last_song` (`Optional[float]`): A timestamp, defining the end of the queue. * `last_song` (`Optional[float]`): A timestamp, defining the end of the queue.
* `waiting_room_policy` (Optional[str]): One of: * `waiting_room_policy` (Optional[str]): One of:
- `force`, if a performer is already in the queue, they are put in the - `forced`, if a performer is already in the queue, they are put in the
waiting room. waiting room.
- `optional`, if a performer is already in the queue, they have the option - `optional`, if a performer is already in the queue, they have the option
to be put in the waiting room. to be put in the waiting room.
@ -132,9 +132,7 @@ class State:
recent: list[Entry] recent: list[Entry]
sid: str sid: str
client: Client client: Client
last_seen: datetime.datetime = field( last_seen: datetime.datetime = field(init=False, default_factory=datetime.datetime.now)
init=False, default_factory=datetime.datetime.now
)
clients: dict[str, State] = {} clients: dict[str, State] = {}
@ -156,13 +154,17 @@ async def send_state(state: State, sid: str) -> None:
:type sid: str: :type sid: str:
:rtype: None :rtype: None
""" """
safe_config = {k: v for k, v in state.client.config.items() if k not in ["secret", "key"]}
print(safe_config)
await sio.emit( await sio.emit(
"state", "state",
{ {
"queue": state.queue, "queue": state.queue,
"recent": state.recent, "recent": state.recent,
"waiting_room": state.waiting_room, "waiting_room": state.waiting_room,
"config": state.client.config, "config": safe_config,
}, },
room=sid, room=sid,
) )
@ -205,18 +207,13 @@ async def handle_waiting_room_append(sid: str, data: dict[str, Any]) -> None:
if entry is None: if entry is None:
await sio.emit( await sio.emit(
"msg", "msg",
{ {"msg": f"Unable to add to the waiting room: {data['ident']}. Maybe try again?"},
"msg": f"Unable to add to the waiting room: {data['ident']}. Maybe try again?"
},
room=sid, room=sid,
) )
return return
if "uid" not in data or ( if "uid" not in data or (
( (data["uid"] is not None and len(list(state.queue.find_by_uid(data["uid"]))) == 0)
data["uid"] is not None
and len(list(state.queue.find_by_uid(data["uid"]))) == 0
)
or (data["uid"] is None and state.queue.find_by_name(data["performer"]) is None) or (data["uid"] is None and state.queue.find_by_name(data["performer"]) is None)
): ):
await append_to_queue(room, entry, sid) await append_to_queue(room, entry, sid)
@ -233,9 +230,7 @@ async def handle_waiting_room_append(sid: str, data: dict[str, Any]) -> None:
) )
async def append_to_queue( async def append_to_queue(room: str, entry: Entry, report_to: Optional[str] = None) -> None:
room: str, entry: Entry, report_to: Optional[str] = None
) -> None:
""" """
Append a song to the queue for a given session. Append a song to the queue for a given session.
@ -259,10 +254,7 @@ async def append_to_queue(
start_time = first_song.started_at start_time = first_song.started_at
start_time = state.queue.fold( start_time = state.queue.fold(
lambda item, time: time lambda item, time: time + item.duration + state.client.config["preview_duration"] + 1,
+ item.duration
+ state.client.config["preview_duration"]
+ 1,
start_time, start_time,
) )
@ -384,7 +376,7 @@ async def handle_append(sid: str, data: dict[str, Any]) -> None:
state = clients[room] state = clients[room]
if state.client.config["waiting_room_policy"] and ( if state.client.config["waiting_room_policy"] and (
state.client.config["waiting_room_policy"].lower() == "force" state.client.config["waiting_room_policy"].lower() == "forced"
or state.client.config["waiting_room_policy"].lower() == "optional" or state.client.config["waiting_room_policy"].lower() == "optional"
): ):
old_entry = state.queue.find_by_name(data["performer"]) old_entry = state.queue.find_by_name(data["performer"])
@ -437,7 +429,7 @@ async def handle_append_anyway(sid: str, data: dict[str, Any]) -> None:
room = session["room"] room = session["room"]
state = clients[room] state = clients[room]
if state.client.config["waiting_room_policy"].lower() == "force": if state.client.config["waiting_room_policy"].lower() == "forced":
await sio.emit( await sio.emit(
"err", "err",
{"type": "WAITING_ROOM_FORCED"}, {"type": "WAITING_ROOM_FORCED"},
@ -546,11 +538,7 @@ async def handle_waiting_room_to_queue(sid: str, data: dict[str, Any]) -> None:
if is_admin: if is_admin:
entry = next( entry = next(
( (wr_entry for wr_entry in state.waiting_room if str(wr_entry.uuid) == data["uuid"]),
wr_entry
for wr_entry in state.waiting_room
if str(wr_entry.uuid) == data["uuid"]
),
None, None,
) )
if entry is not None: if entry is not None:
@ -682,22 +670,19 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None:
""" """
def gen_id(length: int = 4) -> str: def gen_id(length: int = 4) -> str:
client_id = "".join( client_id = "".join([random.choice(string.ascii_letters) for _ in range(length)])
[random.choice(string.ascii_letters) for _ in range(length)]
)
if client_id in clients: if client_id in clients:
client_id = gen_id(length + 1) client_id = gen_id(length + 1)
return client_id return client_id
if not app["public"]: if not app["public"]:
with open(app["registration-keyfile"]) as f: with open(app["registration-keyfile"], encoding="utf8") as f:
raw_keys = f.readlines() raw_keys = f.readlines()
keys = [key[:64] for key in raw_keys] keys = [key[:64] for key in raw_keys]
if ( if (
"registration-key" not in data "key" not in data["config"]
or hashlib.sha256(data["registration-key"].encode()).hexdigest() or hashlib.sha256(data["config"]["key"].encode()).hexdigest() not in keys
not in keys
): ):
await sio.emit( await sio.emit(
"client-registered", "client-registered",
@ -707,9 +692,7 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None:
return return
room: str = ( room: str = (
data["config"]["room"] data["config"]["room"] if "room" in data["config"] and data["config"]["room"] else gen_id()
if "room" in data["config"] and data["config"]["room"]
else gen_id()
) )
async with sio.session(sid) as session: async with sio.session(sid) as session:
session["room"] = room session["room"] = room
@ -725,15 +708,11 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None:
config=DEFAULT_CONFIG | data["config"], config=DEFAULT_CONFIG | data["config"],
) )
await sio.enter_room(sid, room) await sio.enter_room(sid, room)
await sio.emit( await sio.emit("client-registered", {"success": True, "room": room}, room=sid)
"client-registered", {"success": True, "room": room}, room=sid
)
await send_state(clients[room], sid) await send_state(clients[room], sid)
else: else:
logger.warning("Got wrong secret for %s", room) logger.warning("Got wrong secret for %s", room)
await sio.emit( await sio.emit("client-registered", {"success": False, "room": room}, room=sid)
"client-registered", {"success": False, "room": room}, room=sid
)
else: else:
logger.info("Registerd new client %s", room) logger.info("Registerd new client %s", room)
initial_entries = [Entry(**entry) for entry in data["queue"]] initial_entries = [Entry(**entry) for entry in data["queue"]]
@ -824,9 +803,7 @@ async def handle_config_chunk(sid: str, data: dict[str, Any]) -> None:
return return
if data["source"] not in state.client.sources: if data["source"] not in state.client.sources:
state.client.sources[data["source"]] = available_sources[data["source"]]( state.client.sources[data["source"]] = available_sources[data["source"]](data["config"])
data["config"]
)
else: else:
state.client.sources[data["source"]].add_to_config(data["config"]) state.client.sources[data["source"]].add_to_config(data["config"])
@ -855,9 +832,7 @@ async def handle_config(sid: str, data: dict[str, Any]) -> None:
if sid != state.sid: if sid != state.sid:
return return
state.client.sources[data["source"]] = available_sources[data["source"]]( state.client.sources[data["source"]] = available_sources[data["source"]](data["config"])
data["config"]
)
@sio.on("register-web") @sio.on("register-web")
@ -1042,17 +1017,10 @@ async def handle_search(sid: str, data: dict[str, Any]) -> None:
query = data["query"] query = data["query"]
results_list = await asyncio.gather( results_list = await asyncio.gather(
*[ *[state.client.sources[source].search(query) for source in state.client.sources_prio]
state.client.sources[source].search(query)
for source in state.client.sources_prio
]
) )
results = [ results = [search_result for source_result in results_list for search_result in source_result]
search_result
for source_result in results_list
for search_result in source_result
]
await sio.emit( await sio.emit(
"search-results", "search-results",
{"results": results}, {"results": results},
@ -1119,7 +1087,7 @@ def main() -> None:
""" """
parser = ArgumentParser() parser = ArgumentParser()
parser.add_argument("--host", "-H", default="localhost") parser.add_argument("--host", "-H", default="localhost")
parser.add_argument("--port", "-p", default="8080") parser.add_argument("--port", "-p", type=int, default=8080)
parser.add_argument("--root-folder", "-r", default="syng/static/") parser.add_argument("--root-folder", "-r", default="syng/static/")
parser.add_argument("--registration-keyfile", "-k", default=None) parser.add_argument("--registration-keyfile", "-k", default=None)
args = parser.parse_args() args = parser.parse_args()
@ -1131,9 +1099,7 @@ def main() -> None:
app["root_folder"] = args.root_folder app["root_folder"] = args.root_folder
app.add_routes( app.add_routes([web.static("/assets/", os.path.join(app["root_folder"], "assets/"))])
[web.static("/assets/", os.path.join(app["root_folder"], "assets/"))]
)
app.router.add_route("*", "/", root_handler) app.router.add_route("*", "/", root_handler)
app.router.add_route("*", "/{room}", root_handler) app.router.add_route("*", "/{room}", root_handler)

View file

@ -66,7 +66,8 @@ class FileBasedSource(Source):
info: str | MediaInfo = MediaInfo.parse(file) info: str | MediaInfo = MediaInfo.parse(file)
if isinstance(info, str): if isinstance(info, str):
return 180 return 180
return info.audio_tracks[0].to_data()["duration"] // 1000 duration: int = info.audio_tracks[0].to_data()["duration"]
return duration // 1000
video_path, audio_path = self.get_video_audio_split(path) video_path, audio_path = self.get_video_audio_split(path)

View file

@ -107,7 +107,7 @@ class S3Source(FileBasedSource):
await self.ensure_playable(entry) await self.ensure_playable(entry)
file_name: Optional[str] = self.downloaded_files[entry.ident].video file_name: str = self.downloaded_files[entry.ident].video
duration = await self.get_duration(file_name) duration = await self.get_duration(file_name)

View file

@ -120,9 +120,7 @@ class Source(ABC):
source for documentation. source for documentation.
:type config: dict[str, Any] :type config: dict[str, Any]
""" """
self.downloaded_files: defaultdict[str, DLFilesEntry] = defaultdict( self.downloaded_files: defaultdict[str, DLFilesEntry] = defaultdict(DLFilesEntry)
DLFilesEntry
)
self._masterlock: asyncio.Lock = asyncio.Lock() self._masterlock: asyncio.Lock = asyncio.Lock()
self.player: Optional[asyncio.subprocess.Process] = None self.player: Optional[asyncio.subprocess.Process] = None
self._index: list[str] = config["index"] if "index" in config else [] self._index: list[str] = config["index"] if "index" in config else []
@ -145,9 +143,7 @@ class Source(ABC):
:returns: An async reference to the process :returns: An async reference to the process
:rtype: asyncio.subprocess.Process :rtype: asyncio.subprocess.Process
""" """
args = ["--fullscreen", *options, video] + ( args = ["--fullscreen", *options, video] + ([f"--audio-file={audio}"] if audio else [])
[f"--audio-file={audio}"] if audio else []
)
print(f"File is {video=} and {audio=}") print(f"File is {video=} and {audio=}")
@ -231,7 +227,6 @@ class Source(ABC):
:returns: A Tuple of the locations for the video and the audio file. :returns: A Tuple of the locations for the video and the audio file.
:rtype: Tuple[str, Optional[str]] :rtype: Tuple[str, Optional[str]]
""" """
...
async def buffer(self, entry: Entry) -> None: async def buffer(self, entry: Entry) -> None:
""" """
@ -407,7 +402,6 @@ class Source(ABC):
:return: The part of the config, that should be sended to the server. :return: The part of the config, that should be sended to the server.
:rtype: dict[str, Any] | list[dict[str, Any]] :rtype: dict[str, Any] | list[dict[str, Any]]
""" """
print("xzy")
if not self._index: if not self._index:
self._index = [] self._index = []
print(f"{self.source_name}: generating index") print(f"{self.source_name}: generating index")