somewhat new app
This commit is contained in:
parent
03d2c18b29
commit
0fb4af62ec
130
v6d3music/app.py
130
v6d3music/app.py
@ -1,23 +1,61 @@
|
||||
import asyncio
|
||||
import functools
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Any, Callable, Coroutine, Generic, Hashable, TypeVar
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
from aiohttp import web
|
||||
from v6d3music.api import Api
|
||||
from v6d3music.config import auth_redirect, myroot
|
||||
from v6d3music.utils.bytes_hash import bytes_hash
|
||||
|
||||
from ptvp35 import Db, KVJson
|
||||
from v6d0auth.appfactory import AppFactory
|
||||
from v6d0auth.run_app import start_app
|
||||
from v6d1tokens.client import request_token
|
||||
|
||||
from v6d3music.config import auth_redirect, myroot
|
||||
from v6d3music.utils.bytes_hash import bytes_hash
|
||||
from v6d3music.api import Api
|
||||
|
||||
session_db = Db(myroot / 'session.db', kvfactory=KVJson())
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
TKey = TypeVar('TKey', bound=Hashable)
|
||||
|
||||
|
||||
class CachedEntry(Generic[T]):
|
||||
def __init__(self, value: T, getter: Callable[[], Coroutine[Any, Any, T]]) -> None:
|
||||
self.__value: T = value
|
||||
self.__getter = getter
|
||||
self.__task: asyncio.Future[T] = asyncio.Future()
|
||||
self.__task.set_result(value)
|
||||
|
||||
async def _set(self) -> T:
|
||||
self.__value = await self.__getter()
|
||||
return self.__value
|
||||
|
||||
def get_nowait(self) -> T:
|
||||
if self.__task.done():
|
||||
self.__task = asyncio.create_task(self._set())
|
||||
return self.__value
|
||||
|
||||
async def get(self) -> T:
|
||||
if self.__task.done():
|
||||
self.__task = asyncio.create_task(self._set())
|
||||
return await self.__task
|
||||
|
||||
|
||||
class CachedDictionary(Generic[TKey, T]):
|
||||
def __init__(self, factory: Callable[[TKey], Coroutine[Any, Any, T]]) -> None:
|
||||
self.__factory = factory
|
||||
self.__entries: dict[TKey, CachedEntry[T]] = {}
|
||||
|
||||
def entry(self, key: TKey, default: T) -> CachedEntry[T]:
|
||||
if key not in self.__entries:
|
||||
self.__entries[key] = CachedEntry(default, functools.partial(self.__factory, key))
|
||||
return self.__entries[key]
|
||||
|
||||
|
||||
class MusicAppFactory(AppFactory):
|
||||
htmlroot = Path(__file__).parent / 'html'
|
||||
|
||||
@ -32,6 +70,9 @@ class MusicAppFactory(AppFactory):
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.client = client
|
||||
self._api = api
|
||||
self._token_clients: CachedDictionary[str, dict | None] = CachedDictionary(
|
||||
self._token_client
|
||||
)
|
||||
|
||||
def auth_link(self) -> str:
|
||||
if self.client.user is None:
|
||||
@ -40,28 +81,13 @@ class MusicAppFactory(AppFactory):
|
||||
return f'https://discord.com/api/oauth2/authorize?client_id={self.client.user.id}' \
|
||||
f'&redirect_uri={urllib.parse.quote(self.redirect)}&response_type=code&scope=identify'
|
||||
|
||||
def _path(self, file: str):
|
||||
return self.htmlroot / file
|
||||
|
||||
def _file(self, file: str):
|
||||
with open(self.htmlroot / file) as f:
|
||||
return f.read()
|
||||
|
||||
async def file(self, file: str):
|
||||
return await self.loop.run_in_executor(
|
||||
None,
|
||||
self._file,
|
||||
file
|
||||
)
|
||||
|
||||
async def html_resp(self, file: str):
|
||||
text = await self.file(f'{file}.html')
|
||||
text = text.replace(
|
||||
'$$DISCORD_AUTH$$',
|
||||
self.auth_link()
|
||||
)
|
||||
return web.Response(
|
||||
text=text,
|
||||
content_type='text/html'
|
||||
)
|
||||
|
||||
async def code_token(self, code: str) -> dict:
|
||||
assert self.client.user is not None
|
||||
data = {
|
||||
@ -78,13 +104,7 @@ class MusicAppFactory(AppFactory):
|
||||
async with session.post('https://discord.com/api/oauth2/token', data=data, headers=headers) as response:
|
||||
return await response.json()
|
||||
|
||||
@classmethod
|
||||
async def session_client(cls, data: dict) -> Optional[dict]:
|
||||
match data:
|
||||
case {'token': {'access_token': str() as access_token}}:
|
||||
pass
|
||||
case _:
|
||||
return None
|
||||
async def _token_client(self, access_token: str) -> dict | None:
|
||||
headers = {
|
||||
'Authorization': f'Bearer {access_token}'
|
||||
}
|
||||
@ -92,6 +112,17 @@ class MusicAppFactory(AppFactory):
|
||||
async with session.get('https://discord.com/api/oauth2/@me', headers=headers) as response:
|
||||
return await response.json()
|
||||
|
||||
async def token_client(self, access_token: str) -> dict | None:
|
||||
return self._token_clients.entry(access_token, None).get_nowait()
|
||||
|
||||
async def session_client(self, data: dict) -> dict | None:
|
||||
match data:
|
||||
case {'token': {'access_token': str() as access_token}}:
|
||||
pass
|
||||
case _:
|
||||
return None
|
||||
return await self.token_client(access_token)
|
||||
|
||||
@classmethod
|
||||
def client_status(cls, sclient: dict) -> dict:
|
||||
user = cls.client_user(sclient)
|
||||
@ -109,7 +140,7 @@ class MusicAppFactory(AppFactory):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def user_username_full(cls, user: dict) -> Optional[str]:
|
||||
def user_username_full(cls, user: dict) -> str | None:
|
||||
match user:
|
||||
case {'username': str() as username, 'discriminator': str() as discriminator}:
|
||||
return f'{username}#{discriminator}'
|
||||
@ -117,19 +148,19 @@ class MusicAppFactory(AppFactory):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def client_user(cls, sclient: dict) -> Optional[dict]:
|
||||
def client_user(cls, sclient: dict) -> dict | None:
|
||||
return sclient.get('user')
|
||||
|
||||
@classmethod
|
||||
def user_id(cls, user: dict) -> Optional[str | int]:
|
||||
def user_id(cls, user: dict) -> str | int | None:
|
||||
return user.get('id')
|
||||
|
||||
@classmethod
|
||||
def user_avatar(cls, user: dict) -> Optional[str]:
|
||||
def user_avatar(cls, user: dict) -> str | None:
|
||||
return user.get('avatar')
|
||||
|
||||
@classmethod
|
||||
def user_avatar_url(cls, user: dict) -> Optional[str]:
|
||||
def user_avatar_url(cls, user: dict) -> str | None:
|
||||
cid = cls.user_id(user)
|
||||
if cid is None:
|
||||
return None
|
||||
@ -169,15 +200,19 @@ class MusicAppFactory(AppFactory):
|
||||
|
||||
def define_routes(self, routes: web.RouteTableDef) -> None:
|
||||
@routes.get('/')
|
||||
async def home(_request: web.Request) -> web.Response:
|
||||
return await self.html_resp('home')
|
||||
async def home(_request: web.Request) -> web.StreamResponse:
|
||||
return web.FileResponse(self._path('home.html'))
|
||||
|
||||
@routes.get('/login/')
|
||||
async def login(_request: web.Request) -> web.Response:
|
||||
return await self.html_resp('login')
|
||||
async def login(_request: web.Request) -> web.StreamResponse:
|
||||
return web.FileResponse(self._path('login.html'))
|
||||
|
||||
@routes.get('/authlink/')
|
||||
async def authlink(_request: web.Request) -> web.StreamResponse:
|
||||
return web.Response(text=self.auth_link())
|
||||
|
||||
@routes.get('/auth/')
|
||||
async def auth(request: web.Request) -> web.Response:
|
||||
async def auth(request: web.Request) -> web.StreamResponse:
|
||||
if 'session' in request.query:
|
||||
response = web.HTTPFound('/')
|
||||
session = str(request.query.get('session'))
|
||||
@ -191,7 +226,7 @@ class MusicAppFactory(AppFactory):
|
||||
await session_db.set(session, data)
|
||||
return response
|
||||
else:
|
||||
return await self.html_resp('auth')
|
||||
return web.FileResponse(self._path('auth.html'))
|
||||
|
||||
@routes.get('/state/')
|
||||
async def get_state(request: web.Request) -> web.Response:
|
||||
@ -215,17 +250,12 @@ class MusicAppFactory(AppFactory):
|
||||
)
|
||||
|
||||
@routes.get('/main.js')
|
||||
async def mainjs(_request: web.Request) -> web.Response:
|
||||
return web.Response(
|
||||
text=await self.file('main.js')
|
||||
)
|
||||
async def mainjs(_request: web.Request) -> web.StreamResponse:
|
||||
return web.FileResponse(self._path('main.js'))
|
||||
|
||||
@routes.get('/main.css')
|
||||
async def maincss(_request: web.Request) -> web.Response:
|
||||
return web.Response(
|
||||
text=await self.file('main.css'),
|
||||
content_type='text/css'
|
||||
)
|
||||
async def maincss(_request: web.Request) -> web.StreamResponse:
|
||||
return web.FileResponse(self._path('main.css'))
|
||||
|
||||
@routes.post('/api/')
|
||||
async def api(request: web.Request) -> web.Response:
|
||||
|
@ -3,7 +3,11 @@
|
||||
<link rel="stylesheet" href="/main.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root-container">
|
||||
<div class="sidebars"></div>
|
||||
<div id="root"></div>
|
||||
<div class="sidebars"></div>
|
||||
</div>
|
||||
<script src="/main.js"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
|
@ -2,7 +2,6 @@
|
||||
<div id="root"></div>
|
||||
<script src="/main.js"></script>
|
||||
<script>
|
||||
authbase = '$$DISCORD_AUTH$$';
|
||||
(async () => {
|
||||
const a = await aAuth();
|
||||
root.append(a);
|
||||
|
@ -21,3 +21,24 @@ input {
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#root-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: auto;
|
||||
min-width: min(40em, 100%);
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.sidebars {
|
||||
width: 100%;
|
||||
background: #111;
|
||||
}
|
||||
|
@ -73,10 +73,17 @@ const aLogin = () => {
|
||||
a.innerText = "login";
|
||||
return a;
|
||||
};
|
||||
let authbase;
|
||||
const aAuthLink = async () => {
|
||||
const response = await fetch("/authlink/");
|
||||
return await response.text();
|
||||
};
|
||||
const aAuth = async () => {
|
||||
const a = document.createElement("a");
|
||||
a.href = authbase + "&state=" + (await sessionState());
|
||||
const [authlink, sessionstate] = await Promise.all([
|
||||
aAuthLink(),
|
||||
sessionState(),
|
||||
]);
|
||||
a.href = authlink + "&state=" + sessionstate;
|
||||
a.innerText = "auth";
|
||||
return a;
|
||||
};
|
||||
@ -165,7 +172,7 @@ const aUpdateQueueSetup = async (el) => {
|
||||
})();
|
||||
(async () => {
|
||||
while (true) {
|
||||
await sleep(.25);
|
||||
await sleep(0.25);
|
||||
if (queue !== null) {
|
||||
for (const audio of queue.queuejson) {
|
||||
audio.tce.innerText = audio.ts();
|
||||
|
Loading…
Reference in New Issue
Block a user