more improvements, working test client
This commit is contained in:
parent
bc0d91d972
commit
24e3b0fde2
10 changed files with 489 additions and 139 deletions
|
@ -13,8 +13,16 @@ aiohttp = "^3.8.3"
|
||||||
python-mpv = "^1.0.1"
|
python-mpv = "^1.0.1"
|
||||||
python-socketio = "^5.7.2"
|
python-socketio = "^5.7.2"
|
||||||
minio = "^7.1.12"
|
minio = "^7.1.12"
|
||||||
|
colored = "^1.4.4"
|
||||||
|
mutagen = "^1.46.0"
|
||||||
|
aiocmd = "^0.1.5"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.pyright]
|
||||||
|
exclude = [ ".venv" ]
|
||||||
|
venvPath = "."
|
||||||
|
venv = ".venv"
|
||||||
|
|
100
syng/client.py
100
syng/client.py
|
@ -1,17 +1,18 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import socketio
|
|
||||||
from traceback import print_exc
|
from traceback import print_exc
|
||||||
from json import load
|
from json import load
|
||||||
|
|
||||||
|
import socketio
|
||||||
|
|
||||||
from .sources import Source, configure_sources
|
from .sources import Source, configure_sources
|
||||||
from .entry import Entry
|
from .entry import Entry
|
||||||
|
|
||||||
|
|
||||||
sio = socketio.AsyncClient()
|
sio = socketio.AsyncClient()
|
||||||
|
|
||||||
with open("./syng-client.json") as f:
|
with open("./syng-client.json", encoding="utf8") as f:
|
||||||
source_config = load(f)
|
source_config = load(f)
|
||||||
sources = configure_sources(source_config, client=True)
|
sources: dict[str, Source] = configure_sources(source_config)
|
||||||
|
|
||||||
currentLock = asyncio.Semaphore(0)
|
currentLock = asyncio.Semaphore(0)
|
||||||
state = {
|
state = {
|
||||||
|
@ -20,55 +21,10 @@ state = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def playerTask():
|
|
||||||
"""
|
|
||||||
This task loops forever, and plays the first item in the queue in the appropriate player. Then it removes the first item in the queue and starts over. If no element is in the queue, it waits
|
|
||||||
"""
|
|
||||||
|
|
||||||
while True:
|
|
||||||
await sio.emit("get-next", {})
|
|
||||||
print("Waiting for current")
|
|
||||||
await currentLock.acquire()
|
|
||||||
try:
|
|
||||||
await sources[state["current"].source].play(state["current"].id)
|
|
||||||
except Exception:
|
|
||||||
print_exc()
|
|
||||||
print("Finished playing")
|
|
||||||
|
|
||||||
|
|
||||||
async def bufferTask():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# class BufferThread(Thread):
|
|
||||||
# """
|
|
||||||
# This thread tries to buffer the first not-yet buffered entry in the queue in a loop.
|
|
||||||
# """
|
|
||||||
# def run(self):
|
|
||||||
# while (True):
|
|
||||||
# for entry in self.queue:
|
|
||||||
# if entry.ready.is_set():
|
|
||||||
# continue
|
|
||||||
# try:
|
|
||||||
# entry.source.buffer(entry.id)
|
|
||||||
# except Exception:
|
|
||||||
# print_exc()
|
|
||||||
# entry.failed = True
|
|
||||||
# entry.ready.set()
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
@sio.on("skip")
|
@sio.on("skip")
|
||||||
async def handle_skip():
|
async def handle_skip():
|
||||||
print("Skipping current")
|
print("Skipping current")
|
||||||
await sources[state["current"].source].skip_current()
|
await state["current"].skip_current()
|
||||||
|
|
||||||
|
|
||||||
@sio.on("next")
|
|
||||||
async def handle_next(data):
|
|
||||||
state["current"] = Entry(**data)
|
|
||||||
currentLock.release()
|
|
||||||
print("released lock")
|
|
||||||
|
|
||||||
|
|
||||||
@sio.on("state")
|
@sio.on("state")
|
||||||
|
@ -82,18 +38,58 @@ async def handle_connect():
|
||||||
await sio.emit("register-client", {"secret": "test"})
|
await sio.emit("register-client", {"secret": "test"})
|
||||||
|
|
||||||
|
|
||||||
|
@sio.on("buffer")
|
||||||
|
async def handle_buffer(data):
|
||||||
|
source = sources[data["source"]]
|
||||||
|
meta_info = await source.buffer(Entry(**data))
|
||||||
|
await sio.emit("meta-info", {"uuid": data["uuid"], "meta": meta_info})
|
||||||
|
|
||||||
|
|
||||||
|
@sio.on("play")
|
||||||
|
async def handle_play(data):
|
||||||
|
entry = Entry(**data)
|
||||||
|
print(f"Playing {entry}")
|
||||||
|
try:
|
||||||
|
meta_info = await sources[entry.source].buffer(entry)
|
||||||
|
await sio.emit("meta-info", {"uuid": data["uuid"], "meta": meta_info})
|
||||||
|
state["current"] = sources[entry.source]
|
||||||
|
await sources[entry.source].play(entry)
|
||||||
|
except Exception:
|
||||||
|
print_exc()
|
||||||
|
await sio.emit("pop-then-get-next")
|
||||||
|
|
||||||
|
|
||||||
@sio.on("client-registered")
|
@sio.on("client-registered")
|
||||||
async def handle_register(data):
|
async def handle_register(data):
|
||||||
if data["success"]:
|
if data["success"]:
|
||||||
print("Registered")
|
print("Registered")
|
||||||
await sio.emit("config", {"sources": source_config})
|
await sio.emit("sources", {"sources": list(source_config.keys())})
|
||||||
asyncio.create_task(playerTask())
|
await sio.emit("get-first")
|
||||||
asyncio.create_task(bufferTask())
|
|
||||||
else:
|
else:
|
||||||
print("Registration failed")
|
print("Registration failed")
|
||||||
await sio.disconnect()
|
await sio.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
@sio.on("request-config")
|
||||||
|
async def handle_request_config(data):
|
||||||
|
if data["source"] in sources:
|
||||||
|
config = await sources[data["source"]].get_config()
|
||||||
|
if isinstance(config, list):
|
||||||
|
num_chunks = len(config)
|
||||||
|
for current, chunk in enumerate(config):
|
||||||
|
await sio.emit(
|
||||||
|
"config-chunk",
|
||||||
|
{
|
||||||
|
"source": data["source"],
|
||||||
|
"config": chunk,
|
||||||
|
"number": current + 1,
|
||||||
|
"total": num_chunks,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await sio.emit("config", {"source": data["source"], "config": config})
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
await sio.connect("http://127.0.0.1:8080")
|
await sio.connect("http://127.0.0.1:8080")
|
||||||
await sio.wait()
|
await sio.wait()
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
import uuid
|
from uuid import uuid4, UUID
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Entry:
|
class Entry:
|
||||||
id: str | int
|
id: str
|
||||||
source: str
|
source: str
|
||||||
duration: int
|
duration: int
|
||||||
title: str
|
title: str
|
||||||
artist: str
|
artist: str
|
||||||
performer: str
|
performer: str
|
||||||
failed: bool = False
|
failed: bool = False
|
||||||
uuid: UUID = field(default_factory=uuid.uuid4)
|
uuid: UUID = field(default_factory=uuid4)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def from_source(performer: str, ident: str, source: Source) -> Entry:
|
async def from_source(performer: str, ident: str, source: Source) -> Entry:
|
||||||
|
@ -32,3 +32,6 @@ class Entry:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(entry_dict):
|
def from_dict(entry_dict):
|
||||||
return Entry(**entry_dict)
|
return Entry(**entry_dict)
|
||||||
|
|
||||||
|
def update(self, **kwargs):
|
||||||
|
self.__dict__.update(kwargs)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
from __future__ import annotations
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -7,6 +9,7 @@ class Result:
|
||||||
source: str
|
source: str
|
||||||
title: str
|
title: str
|
||||||
artist: str
|
artist: str
|
||||||
|
album: str
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
|
@ -14,4 +17,17 @@ class Result:
|
||||||
"source": self.source,
|
"source": self.source,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"artist": self.artist,
|
"artist": self.artist,
|
||||||
|
"album": self.album,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_filename(filename, source) -> Result | None:
|
||||||
|
try:
|
||||||
|
splitfile = os.path.basename(filename[:-4]).split(" - ")
|
||||||
|
ident = filename
|
||||||
|
artist = splitfile[0].strip()
|
||||||
|
title = splitfile[1].strip()
|
||||||
|
album = splitfile[2].strip()
|
||||||
|
return Result(ident, source, title, artist, album)
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
150
syng/server.py
150
syng/server.py
|
@ -2,25 +2,27 @@ from __future__ import annotations
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
# from flask import Flask
|
|
||||||
# from flask_socketio import SocketIO, emit # type: ignore
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import socketio
|
import socketio
|
||||||
|
|
||||||
from .entry import Entry
|
from .entry import Entry
|
||||||
from .sources import configure_sources
|
from .sources import Source, available_sources
|
||||||
|
|
||||||
# socketio = SocketIO(app, cors_allowed_origins='*')
|
|
||||||
# sio = socketio.AsyncServer()
|
|
||||||
|
|
||||||
sio = socketio.AsyncServer(cors_allowed_origins="*", logger=True, engineio_logger=True)
|
sio = socketio.AsyncServer(cors_allowed_origins="*", logger=True, engineio_logger=True)
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
sio.attach(app)
|
sio.attach(app)
|
||||||
|
|
||||||
admin_secrets = ["admin"]
|
|
||||||
client_secrets = ["test"]
|
@dataclass
|
||||||
sources = {}
|
class State:
|
||||||
|
admin_secret: str | None
|
||||||
|
sources: dict[str, Source]
|
||||||
|
sources_prio: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
global_state = State(None, {}, [])
|
||||||
|
|
||||||
|
|
||||||
class Queue(deque):
|
class Queue(deque):
|
||||||
|
@ -34,6 +36,14 @@ class Queue(deque):
|
||||||
await sio.emit("state", self.to_dict())
|
await sio.emit("state", self.to_dict())
|
||||||
self.num_of_entries_sem.release()
|
self.num_of_entries_sem.release()
|
||||||
|
|
||||||
|
async def peek(self) -> Entry:
|
||||||
|
async with self.readlock:
|
||||||
|
await self.num_of_entries_sem.acquire()
|
||||||
|
item = super().popleft()
|
||||||
|
super().appendleft(item)
|
||||||
|
self.num_of_entries_sem.release()
|
||||||
|
return item
|
||||||
|
|
||||||
async def popleft(self) -> Entry:
|
async def popleft(self) -> Entry:
|
||||||
async with self.readlock:
|
async with self.readlock:
|
||||||
await self.num_of_entries_sem.acquire()
|
await self.num_of_entries_sem.acquire()
|
||||||
|
@ -49,53 +59,110 @@ queue = Queue()
|
||||||
|
|
||||||
|
|
||||||
@sio.on("get-state")
|
@sio.on("get-state")
|
||||||
async def handle_state(sid, data: dict[str, Any]):
|
async def handle_state(sid, data: dict[str, Any] = {}):
|
||||||
await sio.emit("state", queue.to_dict(), room=sid)
|
await sio.emit("state", queue.to_dict(), room=sid)
|
||||||
|
|
||||||
|
|
||||||
@sio.on("append")
|
@sio.on("append")
|
||||||
async def handle_append(sid, data: dict[str, Any]):
|
async def handle_append(sid, data: dict[str, Any]):
|
||||||
|
|
||||||
print(f"append: {data}")
|
print(f"append: {data}")
|
||||||
source_obj = sources[data["source"]]
|
source_obj = global_state.sources[data["source"]]
|
||||||
entry = await Entry.from_source(data["performer"], data["id"], source_obj)
|
entry = await Entry.from_source(data["performer"], data["id"], source_obj)
|
||||||
await queue.append(entry)
|
await queue.append(entry)
|
||||||
print(f"new state: {queue.to_dict()}")
|
print(f"new state: {queue.to_dict()}")
|
||||||
|
|
||||||
|
await sio.emit(
|
||||||
|
"buffer",
|
||||||
|
entry.to_dict(),
|
||||||
|
room="clients",
|
||||||
|
)
|
||||||
|
|
||||||
@sio.on("get-next")
|
|
||||||
async def handle_next(sid, data: dict[str, Any]):
|
@sio.on("meta-info")
|
||||||
|
async def handle_meta_info(sid, data):
|
||||||
async with sio.session(sid) as session:
|
async with sio.session(sid) as session:
|
||||||
if "client" in session and session["client"]:
|
if "client" in session and session["client"]:
|
||||||
print(f"get-next request from client {sid}")
|
for item in queue:
|
||||||
current = await queue.popleft()
|
if str(item.uuid) == data["uuid"]:
|
||||||
|
item.update(**data["meta"])
|
||||||
|
|
||||||
|
await sio.emit("state", queue.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@sio.on("get-first")
|
||||||
|
async def handle_get_first(sid, data={}):
|
||||||
|
async with sio.session(sid) as session:
|
||||||
|
if "client" in session and session["client"]:
|
||||||
|
current = await queue.peek()
|
||||||
print(f"Sending {current} to client {sid}")
|
print(f"Sending {current} to client {sid}")
|
||||||
print(f"new state: {queue.to_dict()}")
|
await sio.emit("play", current.to_dict(), room=sid)
|
||||||
await sio.emit("next", current.to_dict(), room=sid)
|
|
||||||
|
|
||||||
|
@sio.on("pop-then-get-next")
|
||||||
|
async def handle_pop_then_get_next(sid, data={}):
|
||||||
|
async with sio.session(sid) as session:
|
||||||
|
if "client" in session and session["client"]:
|
||||||
|
await queue.popleft()
|
||||||
|
current = await queue.peek()
|
||||||
|
print(f"Sending {current} to client {sid}")
|
||||||
|
await sio.emit("play", current.to_dict(), room=sid)
|
||||||
|
|
||||||
|
|
||||||
@sio.on("register-client")
|
@sio.on("register-client")
|
||||||
async def handle_register_client(sid, data: dict[str, Any]):
|
async def handle_register_client(sid, data: dict[str, Any]):
|
||||||
if data["secret"] in client_secrets:
|
print(f"Registerd new client {sid}")
|
||||||
print(f"Registerd new client {sid}")
|
global_state.admin_secret = data["secret"]
|
||||||
await sio.save_session(sid, {"client": True})
|
await sio.save_session(sid, {"client": True})
|
||||||
sio.enter_room(sid, "clients")
|
sio.enter_room(sid, "clients")
|
||||||
await sio.emit("client-registered", {"success": True}, room=sid)
|
await sio.emit("client-registered", {"success": True}, room=sid)
|
||||||
else:
|
|
||||||
await sio.emit("client-registered", {"success": False}, room=sid)
|
|
||||||
|
@sio.on("sources")
|
||||||
|
async def handle_sources(sid, data):
|
||||||
|
"""
|
||||||
|
Get the list of sources the client wants to use.
|
||||||
|
Update internal list of sources, remove unused
|
||||||
|
sources and query for a config for all uninitialized sources
|
||||||
|
"""
|
||||||
|
async with sio.session(sid) as session:
|
||||||
|
if "client" in session and session["client"]:
|
||||||
|
unused_sources = global_state.sources.keys() - data["sources"]
|
||||||
|
new_sources = data["sources"] - global_state.sources.keys()
|
||||||
|
|
||||||
|
for source in unused_sources:
|
||||||
|
del global_state.sources[source]
|
||||||
|
|
||||||
|
global_state.sources_prio = data["sources"]
|
||||||
|
|
||||||
|
for name in new_sources:
|
||||||
|
await sio.emit("request-config", {"source": name}, room=sid)
|
||||||
|
|
||||||
|
|
||||||
|
@sio.on("config-chunk")
|
||||||
|
async def handle_config_chung(sid, data):
|
||||||
|
async with sio.session(sid) as session:
|
||||||
|
if "client" in session and session["client"]:
|
||||||
|
if not data["source"] in global_state.sources:
|
||||||
|
global_state.sources[data["source"]] = available_sources[
|
||||||
|
data["source"]
|
||||||
|
](data["config"])
|
||||||
|
else:
|
||||||
|
global_state.sources[data["source"]].add_to_config(data["config"])
|
||||||
|
|
||||||
|
|
||||||
@sio.on("config")
|
@sio.on("config")
|
||||||
async def handle_config(sid, data):
|
async def handle_config(sid, data):
|
||||||
async with sio.session(sid) as session:
|
async with sio.session(sid) as session:
|
||||||
if "client" in session and session["client"]:
|
if "client" in session and session["client"]:
|
||||||
sources.update(configure_sources(data["sources"], client=False))
|
global_state.sources[data["source"]] = available_sources[data["source"]](
|
||||||
print(f"Updated Config: {sources}")
|
data["config"]
|
||||||
|
)
|
||||||
|
print(f"Added source {data['source']}")
|
||||||
|
|
||||||
|
|
||||||
@sio.on("register-admin")
|
@sio.on("register-admin")
|
||||||
async def handle_register_admin(sid, data: dict[str, str]):
|
async def handle_register_admin(sid, data: dict[str, str]):
|
||||||
if data["secret"] in admin_secrets:
|
if global_state.admin_secret and data["secret"] in global_state.admin_secret:
|
||||||
print(f"Registerd new admin {sid}")
|
print(f"Registerd new admin {sid}")
|
||||||
await sio.save_session(sid, {"admin": True})
|
await sio.save_session(sid, {"admin": True})
|
||||||
await sio.emit("register-admin", {"success": True}, room=sid)
|
await sio.emit("register-admin", {"success": True}, room=sid)
|
||||||
|
@ -104,17 +171,23 @@ async def handle_register_admin(sid, data: dict[str, str]):
|
||||||
|
|
||||||
|
|
||||||
@sio.on("get-config")
|
@sio.on("get-config")
|
||||||
async def handle_config(sid, data):
|
async def handle_get_config(sid, data):
|
||||||
async with sio.session(sid) as session:
|
async with sio.session(sid) as session:
|
||||||
if "admin" in session and session["admin"]:
|
if "admin" in session and session["admin"]:
|
||||||
await sio.emit("config", list(sources.keys()))
|
await sio.emit(
|
||||||
|
"config",
|
||||||
|
{
|
||||||
|
name: source.get_config()
|
||||||
|
for name, source in global_state.sources.items()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@sio.on("skip")
|
@sio.on("skip")
|
||||||
async def handle_skip(sid, data={}):
|
async def handle_skip(sid, data={}):
|
||||||
async with sio.session(sid) as session:
|
async with sio.session(sid) as session:
|
||||||
if "admin" in session and session["admin"]:
|
if "admin" in session and session["admin"]:
|
||||||
await sio.emit("skip", room="client")
|
await sio.emit("skip", room="clients")
|
||||||
|
|
||||||
|
|
||||||
@sio.on("disconnect")
|
@sio.on("disconnect")
|
||||||
|
@ -128,9 +201,18 @@ async def handle_disconnect(sid, data={}):
|
||||||
async def handle_search(sid, data: dict[str, str]):
|
async def handle_search(sid, data: dict[str, str]):
|
||||||
print(f"Got search request from {sid}: {data}")
|
print(f"Got search request from {sid}: {data}")
|
||||||
query = data["query"]
|
query = data["query"]
|
||||||
results = []
|
result_futures = []
|
||||||
for source in sources.values():
|
for source in global_state.sources_prio:
|
||||||
results += await source.search(query)
|
loop = asyncio.get_running_loop()
|
||||||
|
search_future = loop.create_future()
|
||||||
|
loop.create_task(global_state.sources[source].search(search_future, query))
|
||||||
|
result_futures.append(search_future)
|
||||||
|
|
||||||
|
results = [
|
||||||
|
search_result
|
||||||
|
for result_future in result_futures
|
||||||
|
for search_result in await result_future
|
||||||
|
]
|
||||||
print(f"Found {len(results)} results")
|
print(f"Found {len(results)} results")
|
||||||
await sio.emit("search-results", [result.to_dict() for result in results], room=sid)
|
await sio.emit("search-results", [result.to_dict() for result in results], room=sid)
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,9 @@ from .youtube import YoutubeSource
|
||||||
from .s3 import S3Source
|
from .s3 import S3Source
|
||||||
|
|
||||||
|
|
||||||
def configure_sources(configs: dict, client) -> dict[str, Source]:
|
def configure_sources(configs: dict) -> dict[str, Source]:
|
||||||
print(available_sources)
|
|
||||||
configured_sources = {}
|
configured_sources = {}
|
||||||
for source, config in configs.items():
|
for source, config in configs.items():
|
||||||
if source in available_sources:
|
if source in available_sources:
|
||||||
configured_sources[source] = available_sources[source](config)
|
configured_sources[source] = available_sources[source](config)
|
||||||
if client:
|
|
||||||
configured_sources[source].init_client()
|
|
||||||
else:
|
|
||||||
configured_sources[source].init_server()
|
|
||||||
return configured_sources
|
return configured_sources
|
||||||
|
|
|
@ -1,33 +1,144 @@
|
||||||
|
from json import load
|
||||||
|
from time import sleep, perf_counter
|
||||||
|
from itertools import zip_longest
|
||||||
|
from threading import Event, Lock
|
||||||
|
from asyncio import Future
|
||||||
|
import os
|
||||||
|
|
||||||
from minio import Minio
|
from minio import Minio
|
||||||
from time import perf_counter
|
from mpv import MPV
|
||||||
|
import mutagen
|
||||||
|
|
||||||
from .source import Source, async_in_thread, available_sources
|
from .source import Source, async_in_thread, available_sources
|
||||||
from ..result import Result
|
from ..result import Result
|
||||||
|
from ..entry import Entry
|
||||||
|
|
||||||
|
|
||||||
class S3Source(Source):
|
class S3Source(Source):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.minio = Minio(
|
|
||||||
config["s3_endpoint"],
|
|
||||||
access_key=config["access_key"],
|
|
||||||
secret_key=config["secret_key"],
|
|
||||||
)
|
|
||||||
self.bucket = config["bucket"]
|
|
||||||
|
|
||||||
def init_server(self):
|
if "endpoint" in config and "access_key" in config and "secret_key" in config:
|
||||||
print("Start indexing")
|
self.minio = Minio(
|
||||||
start = perf_counter()
|
config["endpoint"],
|
||||||
self.index = list(self.minio.list_objects("bucket"))
|
access_key=config["access_key"],
|
||||||
stop = perf_counter()
|
secret_key=config["secret_key"],
|
||||||
print(f"Took {stop - start:0.4f} seconds")
|
)
|
||||||
|
self.bucket = config["bucket"]
|
||||||
|
self.tmp_dir = config["tmp_dir"] if "tmp_dir" in config else "/tmp/syng"
|
||||||
|
|
||||||
|
self.index = [] if "index" not in config else config["index"]
|
||||||
|
self.downloaded_files = {}
|
||||||
|
self.player = None
|
||||||
|
self.masterlock = Lock()
|
||||||
|
|
||||||
|
async def get_entry(self, performer: str, filename: str) -> Entry:
|
||||||
|
res = Result.from_filename(filename, "s3")
|
||||||
|
if res is not None:
|
||||||
|
return Entry(
|
||||||
|
id=filename,
|
||||||
|
source="s3",
|
||||||
|
duration=180,
|
||||||
|
title=res.title,
|
||||||
|
artist=res.artist,
|
||||||
|
performer=performer,
|
||||||
|
)
|
||||||
|
raise RuntimeError(f"Could not parse {filename}")
|
||||||
|
|
||||||
@async_in_thread
|
@async_in_thread
|
||||||
def search(self, query: str) -> list[Result]:
|
def play(self, entry) -> None:
|
||||||
pass
|
while not entry.uuid in self.downloaded_files:
|
||||||
|
sleep(0.1)
|
||||||
|
|
||||||
async def build_index():
|
self.downloaded_files[entry.uuid]["lock"].wait()
|
||||||
pass
|
|
||||||
|
self.player = MPV(
|
||||||
|
input_default_bindings=True,
|
||||||
|
input_vo_keyboard=True,
|
||||||
|
osc=True,
|
||||||
|
fullscreen=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
cdg_file = self.downloaded_files[entry.uuid]["cdg"]
|
||||||
|
mp3_file = self.downloaded_files[entry.uuid]["mp3"]
|
||||||
|
|
||||||
|
self.player.loadfile(
|
||||||
|
cdg_file,
|
||||||
|
mode="replace",
|
||||||
|
audio_file=mp3_file,
|
||||||
|
scale="oversample",
|
||||||
|
)
|
||||||
|
self.player.wait_for_playback()
|
||||||
|
self.player.terminate()
|
||||||
|
|
||||||
|
@async_in_thread
|
||||||
|
def get_config(self):
|
||||||
|
if not self.index:
|
||||||
|
print("Start indexing")
|
||||||
|
start = perf_counter()
|
||||||
|
# self.index = [
|
||||||
|
# obj.object_name
|
||||||
|
# for obj in self.minio.list_objects(self.bucket, recursive=True)
|
||||||
|
# ]
|
||||||
|
with open("s3_files", "r") as f:
|
||||||
|
self.index = [item for item in load(f) if item.endswith(".cdg")]
|
||||||
|
print(len(self.index))
|
||||||
|
stop = perf_counter()
|
||||||
|
print(f"Took {stop - start:0.4f} seconds")
|
||||||
|
|
||||||
|
chunked = zip_longest(*[iter(self.index)] * 1000, fillvalue="")
|
||||||
|
return [{"index": list(filter(lambda x: x != "", chunk))} for chunk in chunked]
|
||||||
|
|
||||||
|
def add_to_config(self, config):
|
||||||
|
self.index += config["index"]
|
||||||
|
|
||||||
|
@async_in_thread
|
||||||
|
def search(self, result_future: Future, query: str) -> None:
|
||||||
|
print("searching s3")
|
||||||
|
filtered = self.filter_data_by_query(query, self.index)
|
||||||
|
results = []
|
||||||
|
for filename in filtered:
|
||||||
|
print(filename)
|
||||||
|
result = Result.from_filename(filename, "s3")
|
||||||
|
print(result)
|
||||||
|
if result is None:
|
||||||
|
continue
|
||||||
|
results.append(result)
|
||||||
|
print(results)
|
||||||
|
result_future.set_result(results)
|
||||||
|
|
||||||
|
@async_in_thread
|
||||||
|
def buffer(self, entry: Entry) -> dict:
|
||||||
|
with self.masterlock:
|
||||||
|
if entry.uuid in self.downloaded_files:
|
||||||
|
return {}
|
||||||
|
self.downloaded_files[entry.uuid] = {"lock": Event()}
|
||||||
|
|
||||||
|
cdg_filename = os.path.basename(entry.id)
|
||||||
|
path_to_file = os.path.dirname(entry.id)
|
||||||
|
|
||||||
|
cdg_path_with_uuid = os.path.join(path_to_file, f"{entry.uuid}-{cdg_filename}")
|
||||||
|
target_file_cdg = os.path.join(self.tmp_dir, cdg_path_with_uuid)
|
||||||
|
|
||||||
|
ident_mp3 = entry.id[:-3] + "mp3"
|
||||||
|
target_file_mp3 = target_file_cdg[:-3] + "mp3"
|
||||||
|
os.makedirs(os.path.dirname(target_file_cdg), exist_ok=True)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f'self.minio.fget_object("{self.bucket}", "{entry.id}", "{target_file_cdg}")'
|
||||||
|
)
|
||||||
|
self.minio.fget_object(self.bucket, entry.id, target_file_cdg)
|
||||||
|
self.minio.fget_object(self.bucket, ident_mp3, target_file_mp3)
|
||||||
|
|
||||||
|
self.downloaded_files[entry.uuid]["cdg"] = target_file_cdg
|
||||||
|
self.downloaded_files[entry.uuid]["mp3"] = target_file_mp3
|
||||||
|
self.downloaded_files[entry.uuid]["lock"].set()
|
||||||
|
|
||||||
|
meta_infos = mutagen.File(target_file_mp3).info
|
||||||
|
|
||||||
|
print(f"duration is {meta_infos.length}")
|
||||||
|
|
||||||
|
return {"duration": int(meta_infos.length)}
|
||||||
|
|
||||||
|
|
||||||
available_sources["s3"] = S3Source
|
available_sources["s3"] = S3Source
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import shlex
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Callable, Awaitable
|
from typing import Callable, Awaitable
|
||||||
|
import os.path
|
||||||
|
|
||||||
from ..entry import Entry
|
from ..entry import Entry
|
||||||
from ..result import Result
|
from ..result import Result
|
||||||
|
@ -15,25 +17,41 @@ def async_in_thread(func: Callable) -> Awaitable:
|
||||||
|
|
||||||
|
|
||||||
class Source:
|
class Source:
|
||||||
async def get_entry(self, performer: str, ident: int | str) -> Entry:
|
async def get_entry(self, performer: str, ident: str) -> Entry:
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
async def search(self, query: str) -> list[Result]:
|
async def search(self, result_future: asyncio.Future, query: str) -> None:
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
async def buffer(self, ident: int | str) -> None:
|
async def buffer(self, entry: Entry) -> dict:
|
||||||
pass
|
return {}
|
||||||
|
|
||||||
async def play(self, ident: str) -> None:
|
async def play(self, entry: Entry) -> None:
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
async def skip_current(self) -> None:
|
async def skip_current(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def init_server(self) -> None:
|
async def init_server(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def init_client(self) -> None:
|
async def init_client(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def filter_data_by_query(self, query: str, data: list[str]) -> list[str]:
|
||||||
|
def contains_all_words(words: list[str], element: str) -> bool:
|
||||||
|
for word in words:
|
||||||
|
if not word.lower() in os.path.basename(element).lower():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
splitquery = shlex.split(query)
|
||||||
|
return [element for element in data if contains_all_words(splitquery, element)]
|
||||||
|
|
||||||
|
async def get_config(self) -> dict:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def add_to_config(self, config) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,19 +15,24 @@ class YoutubeSource(Source):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.innertube_client = innertube.InnerTube(client="WEB")
|
self.innertube_client = innertube.InnerTube(client="WEB")
|
||||||
self.channels = config["channels"] if "channels" in config else []
|
self.channels = config["channels"] if "channels" in config else []
|
||||||
|
self.player = None
|
||||||
|
|
||||||
|
async def get_config(self):
|
||||||
|
return {"channels": self.channels}
|
||||||
|
|
||||||
@async_in_thread
|
@async_in_thread
|
||||||
def play(self, ident: str) -> None:
|
def play(self, entry: Entry) -> None:
|
||||||
player = MPV(
|
self.player = MPV(
|
||||||
input_default_bindings=True,
|
input_default_bindings=True,
|
||||||
input_vo_keyboard=True,
|
input_vo_keyboard=True,
|
||||||
osc=True,
|
osc=True,
|
||||||
ytdl=True,
|
ytdl=True,
|
||||||
script_opts="ytdl_hook-ytdl_path=yt-dlp",
|
script_opts="ytdl_hook-ytdl_path=yt-dlp",
|
||||||
|
fullscreen=True,
|
||||||
)
|
)
|
||||||
player.play(ident)
|
self.player.play(entry.id)
|
||||||
player.wait_for_playback()
|
self.player.wait_for_playback()
|
||||||
del player
|
self.player.terminate()
|
||||||
|
|
||||||
async def skip_current(self) -> None:
|
async def skip_current(self) -> None:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
@ -56,24 +61,28 @@ class YoutubeSource(Source):
|
||||||
return 1 - (hits / len(queries))
|
return 1 - (hits / len(queries))
|
||||||
|
|
||||||
@async_in_thread
|
@async_in_thread
|
||||||
def search(self, query: str) -> list[Result]:
|
def search(self, result_future: asyncio.Future, query: str) -> None:
|
||||||
|
results = []
|
||||||
for channel in self.channels:
|
for channel in self.channels:
|
||||||
results = self._channel_search(query, channel)
|
results += self._channel_search(query, channel)
|
||||||
results += Search(query + " karaoke").results
|
results += Search(query + " karaoke").results
|
||||||
|
|
||||||
results.sort(key=partial(self._contains_index, query))
|
results.sort(key=partial(self._contains_index, query))
|
||||||
|
|
||||||
return [
|
result_future.set_result(
|
||||||
Result(
|
[
|
||||||
id=result.watch_url,
|
Result(
|
||||||
source="youtube",
|
id=result.watch_url,
|
||||||
title=result.title,
|
source="youtube",
|
||||||
artist=result.author,
|
title=result.title,
|
||||||
)
|
artist=result.author,
|
||||||
for result in results
|
album="YouTube",
|
||||||
]
|
)
|
||||||
|
for result in results
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def _channel_search(self, query, channel):
|
def _channel_search(self, query, channel) -> list:
|
||||||
browseID = Channel(f"https://www.youtube.com{channel}").channel_id
|
browseID = Channel(f"https://www.youtube.com{channel}").channel_id
|
||||||
endpoint = f"{self.innertube_client.base_url}/browse"
|
endpoint = f"{self.innertube_client.base_url}/browse"
|
||||||
|
|
||||||
|
|
112
syng/webclientmockup.py
Normal file
112
syng/webclientmockup.py
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import socketio
|
||||||
|
import asyncio
|
||||||
|
from .result import Result
|
||||||
|
from .entry import Entry
|
||||||
|
from aiocmd import aiocmd
|
||||||
|
|
||||||
|
sio = socketio.AsyncClient()
|
||||||
|
|
||||||
|
|
||||||
|
@sio.on("search-results")
|
||||||
|
async def handle_search_results(data):
|
||||||
|
for raw_item in data:
|
||||||
|
item = Result(**raw_item)
|
||||||
|
print(f"{item.artist} - {item.title} [{item.album}]")
|
||||||
|
print(f"{item.source}: {item.id}")
|
||||||
|
|
||||||
|
|
||||||
|
@sio.on("state")
|
||||||
|
async def handle_state(data):
|
||||||
|
print("New Queue")
|
||||||
|
for raw_item in data:
|
||||||
|
item = Entry(**raw_item)
|
||||||
|
print(f"\t{item.performer}: {item.artist} - {item.title} ({item.duration})")
|
||||||
|
|
||||||
|
|
||||||
|
@sio.on("connect")
|
||||||
|
async def handle_connect():
|
||||||
|
print("Connected")
|
||||||
|
# await sio.emit("search", {"query": "Linkin Park"})
|
||||||
|
# await sio.emit(
|
||||||
|
# "append",
|
||||||
|
# {
|
||||||
|
# "performer": "Hammy",
|
||||||
|
# "source": "youtube",
|
||||||
|
# "id": "https://www.youtube.com/watch?v=rqZqHXJm-UA", # https://youtube.com/watch?v=x5bM5Bdizi4",
|
||||||
|
# },
|
||||||
|
# )
|
||||||
|
# await sio.emit(
|
||||||
|
# "append",
|
||||||
|
# {
|
||||||
|
# "performer": "Hammy",
|
||||||
|
# "source": "s3",
|
||||||
|
# "id": "Sunfly Gold/SFGD034 - Linkin Park & Limp Bizkit/Linkin Park - Pushing Me Away - Sunfly Gold 34.cdg",
|
||||||
|
# },
|
||||||
|
# )
|
||||||
|
# await sio.emit(
|
||||||
|
# "append",
|
||||||
|
# {
|
||||||
|
# "performer": "Hammy",
|
||||||
|
# "source": "s3",
|
||||||
|
# "id": "Sunfly Gold/SFGD034 - Linkin Park & Limp Bizkit/Linkin Park - Pushing Me Away - Sunfly Gold 34.cdg",
|
||||||
|
# },
|
||||||
|
# )
|
||||||
|
# await sio.emit(
|
||||||
|
# "append",
|
||||||
|
# {
|
||||||
|
# "performer": "Hammy",
|
||||||
|
# "source": "s3",
|
||||||
|
# "id": "Sunfly Gold/SFGD034 - Linkin Park & Limp Bizkit/Linkin Park - Pushing Me Away - Sunfly Gold 34.cdg",
|
||||||
|
# },
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
@sio.on("register-admin")
|
||||||
|
async def handle_register_admin(data):
|
||||||
|
if data["success"]:
|
||||||
|
print("Logged in")
|
||||||
|
else:
|
||||||
|
print("Log in failed")
|
||||||
|
|
||||||
|
|
||||||
|
class SyngShell(aiocmd.PromptToolkitCmd):
|
||||||
|
prompt = "syng> "
|
||||||
|
|
||||||
|
def do_exit(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def do_stuff(self):
|
||||||
|
await sio.emit(
|
||||||
|
"append",
|
||||||
|
{
|
||||||
|
"performer": "Hammy",
|
||||||
|
"source": "youtube",
|
||||||
|
"id": "https://www.youtube.com/watch?v=rqZqHXJm-UA", # https://youtube.com/watch?v=x5bM5Bdizi4",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def do_search(self, query):
|
||||||
|
await sio.emit("search", {"query": query})
|
||||||
|
|
||||||
|
async def do_append(self, source, ident):
|
||||||
|
await sio.emit("append", {"performer": "Hammy", "source": source, "id": ident})
|
||||||
|
|
||||||
|
async def do_admin(self, data):
|
||||||
|
await sio.emit("register-admin", {"secret": data})
|
||||||
|
|
||||||
|
async def do_connect(self):
|
||||||
|
await sio.connect("http://127.0.0.1:8080")
|
||||||
|
|
||||||
|
async def do_skip(self):
|
||||||
|
await sio.emit("skip")
|
||||||
|
|
||||||
|
async def do_queue(self):
|
||||||
|
await sio.emit("get-state")
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
await sio.connect("http://127.0.0.1:8080")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(SyngShell().run())
|
Loading…
Add table
Reference in a new issue