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
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"]

View file

@ -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

View file

@ -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))

View file

@ -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("<KeyRelease>", 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:
def save_config(self) -> None:
with open(self.configfile, "w", encoding="utf-8") as f:
dump(self.gather_config(), f, Dumper=Dumper)
def gather_config(self) -> dict[str, Any]:
sources = {}
for source, tab in self.tabs.items():
sources[source] = tab.get_config()
general_config = self.general_config.get_config()
config = {"sources": sources, "config": general_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,)
)
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:

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.
* `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)

View file

@ -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)

View file

@ -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)

View file

@ -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")