Разработан веб-сервер и расширение для браузеров на базе Chromium

This commit is contained in:
2024-07-26 00:33:19 +03:00
commit beb1559cd4
14 changed files with 490 additions and 0 deletions

1
server/api/__init__.py Normal file
View File

@ -0,0 +1 @@
from .main import app

151
server/api/main.py Normal file
View File

@ -0,0 +1,151 @@
import asyncio
import hashlib
import os
import aiohttp
from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
from fastapi.exceptions import HTTPException
from fastapi.responses import Response, HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from jinja2 import Environment, FileSystemLoader
import config
app = FastAPI()
app.mount(
path='/static',
app=StaticFiles(
directory=os.path.join(
config.Main.working_directory,
'static',
),
),
)
env = Environment(
loader=FileSystemLoader(
searchpath=os.path.join(
'.',
'templates',
),
),
enable_async=True,
)
class ConnectionManager:
def __init__(self):
self.connections: list[WebSocket] = list[WebSocket]()
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.connections.remove(websocket)
async def broadcast(self, data: dict):
for connection in self.connections:
asyncio.ensure_future(connection.send_json(data))
overlay_manager = ConnectionManager()
plugin_manager = ConnectionManager()
@app.options(
path='/api/v1/overlay',
)
async def function():
return Response(
headers={
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': '*',
},
status_code=200,
)
@app.get(
path='/overlay',
)
async def function(
justify_content: str = Query(
default='end',
alias='justify-content',
),
align_items: str = Query(
default='end',
alias='align-items',
),
):
template = env.get_template('overlay.jinja2')
return HTMLResponse(
content=await template.render_async(
justify_content=justify_content,
align_items=align_items,
),
status_code=200,
)
@app.get(
path='/image',
)
async def function(
src: str = Query(
default=...,
),
):
image_name = hashlib.sha1(src.encode('ascii')).hexdigest()
image_path = os.path.join(
config.Main.images_directory,
image_name,
)
if not os.path.exists(image_path):
os.makedirs(
name=config.Main.images_directory,
exist_ok=True,
)
async with aiohttp.ClientSession() as session:
async with session.get(src) as response:
if not response.ok:
raise HTTPException(
status_code=response.status,
)
with open(
file=image_path,
mode='wb',
) as file:
file.write(await response.read())
return FileResponse(
path=image_path,
)
@app.websocket('/ws/v1/overlay')
async def function(
websocket: WebSocket,
):
await overlay_manager.connect(websocket)
try:
while True:
data = await websocket.receive_json()
await plugin_manager.broadcast(data)
except WebSocketDisconnect:
overlay_manager.disconnect(websocket)
@app.websocket('/ws/v1/plugin')
async def function(
websocket: WebSocket,
):
await plugin_manager.connect(websocket)
try:
while True:
data = await websocket.receive_json()
await overlay_manager.broadcast(data)
except WebSocketDisconnect:
plugin_manager.disconnect(websocket)

3
server/config.ini Normal file
View File

@ -0,0 +1,3 @@
[Main]
host = 127.0.0.1
port = 8000

View File

@ -0,0 +1 @@
from .main import Main

43
server/config/main.py Normal file
View File

@ -0,0 +1,43 @@
from configparser import RawConfigParser
from enum import Enum
import os
config = RawConfigParser()
config.optionxform = str
config.read(
filenames='./config.ini',
)
class Section(Enum):
main = 'Main'
class Main:
host = config.get(
section=Section.main.value,
option='host',
fallback='127.0.0.1',
)
port = config.getint(
section=Section.main.value,
option='port',
fallback=8000,
)
working_directory = config.get(
section=Section.main.value,
option='working_directory',
fallback=os.getcwd(),
)
images_directory = config.get(
section=Section.main.value,
option='images_directory',
fallback=os.path.join(
os.getenv('APPDATA'),
'csasq',
'YT Music Live',
'cache',
'covers',
),
)

BIN
server/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

17
server/main.py Normal file
View File

@ -0,0 +1,17 @@
import shutil
import uvicorn
from api import app
import config
if __name__ == '__main__':
try:
uvicorn.run(
app=app,
host=config.Main.host,
port=config.Main.port,
)
except InterruptedError:
shutil.rmtree(config.Main.images_directory)

6
server/requirements.txt Normal file
View File

@ -0,0 +1,6 @@
aiohttp
fastapi
jinja2
Nuitka
uvicorn
websockets

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,41 @@
const colorThief = new ColorThief();
const musicElement = document.querySelector('#music');
const progressBarElement = musicElement.querySelector('#music > .progress-bar');
const titleElement = progressBarElement.querySelector('#music > .progress-bar > .data > .title');
const artistsElement = progressBarElement.querySelector('#music > .progress-bar > .data > .artists');
const coverElement = progressBarElement.querySelector('#music > .progress-bar > .cover');
const handlers = {
'target': (attributes) => {
const target = document.getElementById(attributes.id);
target.style.setProperty('--progress-size', `${attributes.progress}`);
},
'music': (attributes) => {
titleElement.textContent = attributes.title;
artistsElement.textContent = attributes.artists;
musicElement.style.setProperty('--progress-size', `${(1 - attributes.progress) * 100}%`);
const url = new URL(location);
url.pathname = '/image';
const urlSearchParams = new URLSearchParams();
urlSearchParams.set('src', attributes.image);
url.search = urlSearchParams.toString();
coverElement.src = url;
const colors = colorThief.getPalette(coverElement, 2);
progressBarElement.style.setProperty('--primary-color', `rgb(${colors[0][0]}, ${colors[0][1]}, ${colors[0][2]})`);
progressBarElement.style.setProperty('--secondary-color', `rgb(${colors[1][0]}, ${colors[1][1]}, ${colors[1][2]})`);
musicElement.classList.remove('hidden');
},
};
const connectWebSocket = () => {
const ws = new WebSocket('ws://localhost:8000/ws/v1/overlay');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handlers[data.type](data.attributes);
};
ws.onclose = () => connectWebSocket();
};
connectWebSocket();

View File

@ -0,0 +1,130 @@
@property --primary-color {
syntax: '<color>';
initial-value: white;
inherits: false;
}
@property --secondary-color {
syntax: '<color>';
initial-value: white;
inherits: false;
}
:root {
--text-shadow: 0 0 12px rgba(0, 0, 0, 0.4);
--box-shadow: 0 .5rem .75rem .375rem rgba(0, 0, 0, .15), 0 .25rem .25rem rgba(0, 0, 0, .3);
}
* {
font-family: 'Exo 2', sans-serif;
user-select: none;
}
html {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
body {
padding: 2rem;
}
#root {
display: flex;
flex-direction: column;
align-items: center;
}
#root > * {
width: 30rem;
}
#root > *:not(:first-child) {
margin-top: 2rem;
}
#root > *.hidden {
display: none;
}
#root > * > .title {
margin-bottom: .5rem;
color: white;
text-shadow: var(--text-shadow);
font-size: 1.5rem;
font-weight: 600;
}
#music {
display: flex;
flex-direction: column;
--progress: 100%;
}
#music > .progress-bar {
position: relative;
display: flex;
align-items: center;
margin-top: .5rem;
padding: 1rem;
width: 28rem;
background-image: linear-gradient(225deg, var(--primary-color), var(--secondary-color));
box-shadow: var(--box-shadow);
border-radius: .5rem;
overflow: hidden;
transition:
opacity .4s,
--primary-color 1s,
--secondary-color 1s;
--progress-size: inherit;
--progress-color: inherit;
}
#music > .progress-bar::before {
content: '';
position: absolute;
top: 0;
right: var(--progress-size);
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, .3);
transition:
right 1s,
background-color 4s;
}
#music > .progress-bar > .data {
display: flex;
flex-direction: column;
margin-left: 1rem;
overflow-x: hidden;
z-index: 1;
}
#music > .progress-bar > .data > .title,
#music > .progress-bar > .data > .artists {
color: white;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
}
#music > .progress-bar > .data > .title {
font-size: 1.2rem;
font-weight: 600;
}
#music > .progress-bar > .data > .artists {
font-size: 1rem;
font-weight: 400;
}
#music > .progress-bar > .cover {
flex-shrink: 0;
width: 3rem;
height: 3rem;
object-fit: cover;
z-index: 1;
}

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="ru-RU">
<head>
<meta charset="UTF-8" />
<title></title>
<link rel="stylesheet" href="/static/styles/main.css" />
<style>
html {
justify-content: {{ justify_content }};
align-items: {{ align_items }};
}
</style>
<script src="/static/scripts/color-thief.umd.js"></script>
</head>
<body>
<div id="root">
<div id="music" class="hidden">
<span class="title">Сейчас играет</span>
<div class="progress-bar">
<img class="cover" />
<div class="data">
<span class="title"></span>
<span class="artists"></span>
</div>
</div>
</div>
</div>
</body>
<script src="/static/scripts/main.js"></script>
</html>