diff --git a/customtkinter.pyi b/customtkinter.pyi new file mode 100644 index 0000000..d0b5aeb --- /dev/null +++ b/customtkinter.pyi @@ -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: ... diff --git a/docs/source/conf.py b/docs/source/conf.py index 3246357..0c1c1b3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,6 +6,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information import os import sys +import sphinx_rtd_theme sys.path.insert(0, os.path.abspath("..")) @@ -17,7 +18,7 @@ release = "2.0.0" # -- 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"] exclude_patterns = [] @@ -26,5 +27,5 @@ exclude_patterns = [] # -- 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"] diff --git a/pyproject.toml b/pyproject.toml index 7b7f246..c475383 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,9 @@ pyyaml = "^6.0.1" async-tkinter-loop = "^0.9.2" tkcalendar = "^1.6.1" tktimepicker = "^2.0.2" +types-pyyaml = "^6.0.12.12" +types-pillow = "^10.1.0.2" +platformdirs = "^4.0.0" [build-system] requires = ["poetry-core"] @@ -40,17 +43,20 @@ exclude = [ ".venv" ] venvPath = "." venv = ".venv" +[tool.pylint."MESSAGES CONTROL"] +disable = '''too-many-lines, +too-many-ancestors +''' + [[tool.mypy.overrides]] module = [ - "aiohttp", - "pytube", - "minio", - "aiocmd", - "pyqrcodeng", - "socketio", - "pillow", - "PIL", "yt_dlp", + "pymediainfo", + "minio", + "qrcode", + "engineio", + "tkcalendar", + "tktimepicker" ] ignore_missing_imports = true diff --git a/syng/client.py b/syng/client.py index 48d216e..3017eb6 100644 --- a/syng/client.py +++ b/syng/client.py @@ -16,19 +16,16 @@ Excerp from the help:: --config-file CONFIG_FILE, -C CONFIG_FILE --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": { - "SOURCE1": { configuration for SOURCE }, - "SOURCE2": { configuration for SOURCE }, + sources: + SOURCE1: + configuration for SOURCE + SOURCE2: + configuration for SOURCE ... - }, - }, - "config": { + config: configuration for the client - } - } """ import asyncio import datetime @@ -40,21 +37,19 @@ import signal from argparse import ArgumentParser from dataclasses import dataclass from dataclasses import field -from yaml import load, Loader from traceback import print_exc -from typing import Any -from typing import Optional +from typing import Any, Optional import qrcode import socketio import engineio from PIL import Image +from yaml import load, Loader from . import jsonencoder from .entry import Entry -from .sources import configure_sources -from .sources import Source +from .sources import configure_sources, Source sio: socketio.AsyncClient = socketio.AsyncClient(json=jsonencoder) @@ -106,7 +101,7 @@ class State: * `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 + - `forced`, 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. @@ -200,12 +195,8 @@ async def handle_connect() -> None: "queue": state.queue, "waiting_room": state.waiting_room, "recent": state.recent, - # "room": state.config["room"], - # "secret": state.config["secret"], "config": state.config, } - if state.config["key"]: - data["registration-key"] = state.config["key"] # TODO: unify await sio.emit("register-client", data) @@ -357,9 +348,7 @@ async def handle_request_config(data: dict[str, Any]) -> None: :rtype: None """ if data["source"] in sources: - config: dict[str, Any] | list[dict[str, Any]] = await sources[ - data["source"] - ].get_config() + config: dict[str, Any] | list[dict[str, Any]] = await sources[data["source"]].get_config() if isinstance(config, list): num_chunks: int = len(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}) -def signal_handler(): +def signal_handler() -> None: engineio.async_client.async_signal_handler() if state.current_source 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() -def create_async_and_start_client(config): +def create_async_and_start_client(config: dict[str, Any]) -> None: asyncio.run(start_client(config)) diff --git a/syng/gui.py b/syng/gui.py index ade1253..e6ec034 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -1,4 +1,4 @@ -import asyncio +from multiprocessing import Process from collections.abc import Callable from datetime import datetime, date, time import os @@ -6,19 +6,19 @@ import builtins from functools import partial from typing import Any, Optional import webbrowser -import PIL -from yaml import load, Loader import multiprocessing -import customtkinter -import qrcode import secrets 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 .server import main as server_main @@ -57,9 +57,7 @@ class DateAndTimePickerWindow(customtkinter.CTkToplevel): self.timepicker.pack(expand=True, fill="both") - button = customtkinter.CTkButton( - self, text="Ok", command=partial(self.insert, input_field) - ) + button = customtkinter.CTkButton(self, text="Ok", command=partial(self.insert, input_field)) button.pack(expand=True, fill="x") def insert(self, input_field: customtkinter.CTkTextbox) -> None: @@ -105,17 +103,13 @@ class OptionFrame(customtkinter.CTkScrollableFrame): description: str, value: str = "", callback: Optional[Callable[..., None]] = None, - ): + ) -> None: self.add_option_label(description) if value is None: value = "" - self.string_options[name] = customtkinter.CTkTextbox( - self, wrap="none", height=1 - ) - self.string_options[name].grid( - column=1, row=self.number_of_options, sticky="EW" - ) + self.string_options[name] = customtkinter.CTkTextbox(self, wrap="none", height=1) + self.string_options[name].grid(column=1, row=self.number_of_options, sticky="EW") self.string_options[name].insert("0.0", value) if callback is not None: self.string_options[name].bind("", callback) @@ -160,7 +154,7 @@ class OptionFrame(customtkinter.CTkScrollableFrame): self, name: str, description: str, - value: list[str] = [], + value: list[str], callback: Optional[Callable[..., None]] = None, ) -> None: self.add_option_label(description) @@ -185,32 +179,26 @@ class OptionFrame(customtkinter.CTkScrollableFrame): ) -> None: self.add_option_label(description) self.choose_options[name] = customtkinter.CTkOptionMenu(self, values=values) - self.choose_options[name].grid( - column=1, row=self.number_of_options, sticky="EW" - ) + self.choose_options[name].grid(column=1, row=self.number_of_options, sticky="EW") self.choose_options[name].set(value) self.number_of_options += 1 - def open_date_and_time_picker( - self, name: str, input_field: customtkinter.CTkTextbox - ) -> None: + def open_date_and_time_picker(self, name: str, input_field: customtkinter.CTkTextbox) -> None: if ( name not in self.date_and_time_pickers or not self.date_and_time_pickers[name].winfo_exists() ): - self.date_and_time_pickers[name] = DateAndTimePickerWindow( - self, input_field - ) + self.date_and_time_pickers[name] = DateAndTimePickerWindow(self, input_field) else: self.date_and_time_pickers[name].focus() def add_date_time_option(self, name: str, description: str, value: str) -> None: self.add_option_label(description) - self.date_time_options[name] = None input_and_button = customtkinter.CTkFrame(self) 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.pack(side="left", fill="x", expand=True) + self.date_time_options[name] = input_field try: datetime.fromisoformat(value) except TypeError: @@ -229,16 +217,16 @@ class OptionFrame(customtkinter.CTkScrollableFrame): def __init__(self, parent: customtkinter.CTkFrame) -> None: super().__init__(parent) self.columnconfigure((1,), weight=1) - self.number_of_options = 0 - self.string_options = {} - self.choose_options = {} - self.bool_options = {} - self.list_options = {} - self.date_time_options = {} - self.date_and_time_pickers = {} + self.number_of_options: int = 0 + self.string_options: dict[str, customtkinter.CTkTextbox] = {} + self.choose_options: dict[str, customtkinter.CTkOptionMenu] = {} + self.bool_options: dict[str, customtkinter.CTkCheckBox] = {} + self.list_options: dict[str, list[customtkinter.CTkTextbox]] = {} + self.date_time_options: dict[str, customtkinter.CTkTextbox] = {} + self.date_and_time_pickers: dict[str, DateAndTimePickerWindow] = {} def get_config(self) -> dict[str, Any]: - config = {} + config: dict[str, Any] = {} for name, textbox in self.string_options.items(): config[name] = textbox.get("0.0", "end").strip() @@ -253,6 +241,9 @@ class OptionFrame(customtkinter.CTkScrollableFrame): for textbox in textboxes: 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 @@ -293,9 +284,7 @@ class GeneralConfig(OptionFrame): str(config["waiting_room_policy"]).lower(), ) self.add_date_time_option("last_song", "Time of last song", config["last_song"]) - self.add_string_option( - "preview_duration", "Preview Duration", config["preview_duration"] - ) + self.add_string_option("preview_duration", "Preview Duration", config["preview_duration"]) def get_config(self) -> dict[str, Any]: config = super().get_config() @@ -308,15 +297,12 @@ class GeneralConfig(OptionFrame): class SyngGui(customtkinter.CTk): - def loadConfig(self) -> None: - filedialog.askopenfilename() - def on_close(self) -> None: - if self.server is not None: - self.server.kill() + if self.syng_server is not None: + self.syng_server.kill() - if self.client is not None: - self.client.kill() + if self.syng_client is not None: + self.syng_client.kill() self.withdraw() self.destroy() @@ -326,21 +312,30 @@ class SyngGui(customtkinter.CTk): self.protocol("WM_DELETE_WINDOW", self.on_close) 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.iconphoto(False, img) - self.server = None - self.client = None + self.syng_server: Optional[Process] = None + self.syng_client: Optional[Process] = None + + self.configfile = os.path.join(platformdirs.user_config_dir("syng"), "config.yaml") try: - with open("syng-client.yaml") as cfile: + with open(self.configfile, encoding="utf8") as cfile: loaded_config = load(cfile, Loader=Loader) except FileNotFoundError: + print("No config found, using default values") loaded_config = {} - config = {"sources": {}, "config": default_config()} - if "config" in loaded_config: + config: dict[str, dict[str, Any]] = {"sources": {}, "config": default_config()} + + try: 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"]: config["config"]["secret"] = "".join( @@ -350,30 +345,26 @@ class SyngGui(customtkinter.CTk): self.wm_title("Syng") # Buttons - fileframe = customtkinter.CTkFrame(self) - fileframe.pack(side="bottom") + button_line = customtkinter.CTkFrame(self) + button_line.pack(side="bottom", fill="x") - loadbutton = customtkinter.CTkButton( - fileframe, - text="load", - command=self.loadConfig, + startsyng_serverbutton = customtkinter.CTkButton( + button_line, text="Start Local Server", command=self.start_syng_server ) - loadbutton.pack(side="left") + startsyng_serverbutton.pack(side="left", expand=True, anchor="w", padx=10, pady=5) - self.startbutton = customtkinter.CTkButton( - fileframe, text="Start", command=self.start_client - ) - self.startbutton.pack(side="right") - - # startserverbutton = customtkinter.CTkButton( - # fileframe, text="Start Server", command=self.start_server - # ) - # startserverbutton.pack(side="right") + savebutton = customtkinter.CTkButton(button_line, text="Save", command=self.save_config) + savebutton.pack(side="left", padx=10, pady=5) 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 frm = customtkinter.CTkFrame(self) @@ -391,7 +382,7 @@ class SyngGui(customtkinter.CTk): self.qrlabel.pack(side="left", anchor="n", padx=10, pady=10) 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) @@ -400,62 +391,67 @@ class SyngGui(customtkinter.CTk): for source_name in available_sources: try: source_config = loaded_config["sources"][source_name] - except KeyError: + except (KeyError, TypeError): source_config = {} - self.tabs[source_name] = SourceTab( - tabview.tab(source_name), source_name, source_config - ) + self.tabs[source_name] = SourceTab(tabview.tab(source_name), source_name, source_config) self.tabs[source_name].pack(ipadx=10, expand=True, fill="both") - self.updateQr() + self.update_qr() - def start_client(self) -> None: - if self.client is None: - sources = {} - for source, tab in self.tabs.items(): - sources[source] = tab.get_config() + def save_config(self) -> None: + with open(self.configfile, "w", encoding="utf-8") as f: + dump(self.gather_config(), f, Dumper=Dumper) - 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} - self.client = multiprocessing.Process( + general_config = self.general_config.get_config() + + 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,) ) - self.client.start() + self.syng_client.start() self.startbutton.configure(text="Stop") else: - self.client.terminate() - self.client = None - self.startbutton.configure(text="Start") + self.syng_client.terminate() + self.syng_client = None + self.startbutton.configure(text="Save and Start") - def start_server(self) -> None: - self.server = multiprocessing.Process(target=server_main) - self.server.start() + def start_syng_server(self) -> None: + self.syng_server = multiprocessing.Process(target=server_main) + self.syng_server.start() def open_web(self) -> None: config = self.general_config.get_config() - server = config["server"] - server += "" if server.endswith("/") else "/" + syng_server = config["server"] + syng_server += "" if syng_server.endswith("/") else "/" room = config["room"] - webbrowser.open(server + room) + webbrowser.open(syng_server + room) - def changeQr(self, data: str) -> None: - qr = qrcode.QRCode(box_size=20, border=2) + def change_qr(self, data: str) -> None: + qr = QRCode(box_size=20, border=2) qr.add_data(data) qr.make() qr.print_ascii() image = qr.make_image().convert("RGB") - tkQrcode = customtkinter.CTkImage(light_image=image, size=(280, 280)) - self.qrlabel.configure(image=tkQrcode) + tk_qrcode = customtkinter.CTkImage(light_image=image, size=(280, 280)) + 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() - server = config["server"] - server += "" if server.endswith("/") else "/" + syng_server = config["server"] + syng_server += "" if syng_server.endswith("/") else "/" room = config["room"] - print(server + room) - self.changeQr(server + room) + print(syng_server + room) + self.change_qr(syng_server + room) def main() -> None: diff --git a/syng/py.typed b/syng/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/syng/server.py b/syng/server.py index 157bf1c..7c33302 100644 --- a/syng/server.py +++ b/syng/server.py @@ -90,7 +90,7 @@ class Client: 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 + - `forced`, 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. @@ -132,9 +132,7 @@ class State: recent: list[Entry] sid: str client: Client - last_seen: datetime.datetime = field( - init=False, default_factory=datetime.datetime.now - ) + last_seen: datetime.datetime = field(init=False, default_factory=datetime.datetime.now) clients: dict[str, State] = {} @@ -156,13 +154,17 @@ async def send_state(state: State, sid: str) -> None: :type sid: str: :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( "state", { "queue": state.queue, "recent": state.recent, "waiting_room": state.waiting_room, - "config": state.client.config, + "config": safe_config, }, room=sid, ) @@ -205,18 +207,13 @@ async def handle_waiting_room_append(sid: str, data: dict[str, Any]) -> None: if entry is None: await sio.emit( "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, ) return 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) ): 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( - room: str, entry: Entry, report_to: Optional[str] = None -) -> None: +async def append_to_queue(room: str, entry: Entry, report_to: Optional[str] = None) -> None: """ 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 = state.queue.fold( - lambda item, time: time - + item.duration - + state.client.config["preview_duration"] - + 1, + lambda item, time: time + item.duration + state.client.config["preview_duration"] + 1, start_time, ) @@ -384,7 +376,7 @@ async def handle_append(sid: str, data: dict[str, Any]) -> None: state = clients[room] 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" ): 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"] state = clients[room] - if state.client.config["waiting_room_policy"].lower() == "force": + if state.client.config["waiting_room_policy"].lower() == "forced": await sio.emit( "err", {"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: 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, ) 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: - 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 if not app["public"]: - with open(app["registration-keyfile"]) as f: + with open(app["registration-keyfile"], encoding="utf8") as f: raw_keys = f.readlines() keys = [key[:64] for key in raw_keys] if ( - "registration-key" not in data - or hashlib.sha256(data["registration-key"].encode()).hexdigest() - not in keys + "key" not in data["config"] + or hashlib.sha256(data["config"]["key"].encode()).hexdigest() not in keys ): await sio.emit( "client-registered", @@ -707,9 +692,7 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None: return room: str = ( - data["config"]["room"] - if "room" in data["config"] and data["config"]["room"] - else gen_id() + data["config"]["room"] if "room" in data["config"] and data["config"]["room"] else gen_id() ) async with sio.session(sid) as session: session["room"] = room @@ -725,15 +708,11 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None: config=DEFAULT_CONFIG | data["config"], ) await sio.enter_room(sid, room) - await sio.emit( - "client-registered", {"success": True, "room": room}, room=sid - ) + await sio.emit("client-registered", {"success": True, "room": room}, room=sid) await send_state(clients[room], sid) else: logger.warning("Got wrong secret for %s", room) - await sio.emit( - "client-registered", {"success": False, "room": room}, room=sid - ) + await sio.emit("client-registered", {"success": False, "room": room}, room=sid) else: logger.info("Registerd new client %s", room) 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 if data["source"] not in state.client.sources: - state.client.sources[data["source"]] = available_sources[data["source"]]( - data["config"] - ) + state.client.sources[data["source"]] = available_sources[data["source"]](data["config"]) else: 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: return - state.client.sources[data["source"]] = available_sources[data["source"]]( - data["config"] - ) + state.client.sources[data["source"]] = available_sources[data["source"]](data["config"]) @sio.on("register-web") @@ -1042,17 +1017,10 @@ async def handle_search(sid: str, data: dict[str, Any]) -> None: query = data["query"] 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 = [ - search_result - for source_result in results_list - for search_result in source_result - ] + results = [search_result for source_result in results_list for search_result in source_result] await sio.emit( "search-results", {"results": results}, @@ -1119,7 +1087,7 @@ def main() -> None: """ parser = ArgumentParser() 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("--registration-keyfile", "-k", default=None) args = parser.parse_args() @@ -1131,9 +1099,7 @@ def main() -> None: app["root_folder"] = args.root_folder - app.add_routes( - [web.static("/assets/", os.path.join(app["root_folder"], "assets/"))] - ) + app.add_routes([web.static("/assets/", os.path.join(app["root_folder"], "assets/"))]) app.router.add_route("*", "/", root_handler) app.router.add_route("*", "/{room}", root_handler) diff --git a/syng/sources/filebased.py b/syng/sources/filebased.py index dc59be0..7df58e5 100644 --- a/syng/sources/filebased.py +++ b/syng/sources/filebased.py @@ -66,7 +66,8 @@ class FileBasedSource(Source): info: str | MediaInfo = MediaInfo.parse(file) if isinstance(info, str): 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) diff --git a/syng/sources/s3.py b/syng/sources/s3.py index f3c103b..f2318a0 100644 --- a/syng/sources/s3.py +++ b/syng/sources/s3.py @@ -107,7 +107,7 @@ class S3Source(FileBasedSource): 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) diff --git a/syng/sources/source.py b/syng/sources/source.py index 2d3d734..217f8e0 100644 --- a/syng/sources/source.py +++ b/syng/sources/source.py @@ -120,9 +120,7 @@ class Source(ABC): source for documentation. :type config: dict[str, Any] """ - self.downloaded_files: defaultdict[str, DLFilesEntry] = defaultdict( - DLFilesEntry - ) + self.downloaded_files: defaultdict[str, DLFilesEntry] = defaultdict(DLFilesEntry) self._masterlock: asyncio.Lock = asyncio.Lock() self.player: Optional[asyncio.subprocess.Process] = None 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 :rtype: asyncio.subprocess.Process """ - args = ["--fullscreen", *options, video] + ( - [f"--audio-file={audio}"] if audio else [] - ) + args = ["--fullscreen", *options, video] + ([f"--audio-file={audio}"] if audio else []) 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. :rtype: Tuple[str, Optional[str]] """ - ... 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. :rtype: dict[str, Any] | list[dict[str, Any]] """ - print("xzy") if not self._index: self._index = [] print(f"{self.source_name}: generating index")