Разработан веб-сервер и расширение для браузеров на базе Chromium
This commit is contained in:
1
server/api/__init__.py
Normal file
1
server/api/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .main import app
|
151
server/api/main.py
Normal file
151
server/api/main.py
Normal 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
3
server/config.ini
Normal file
@ -0,0 +1,3 @@
|
||||
[Main]
|
||||
host = 127.0.0.1
|
||||
port = 8000
|
1
server/config/__init__.py
Normal file
1
server/config/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .main import Main
|
43
server/config/main.py
Normal file
43
server/config/main.py
Normal 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
BIN
server/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
17
server/main.py
Normal file
17
server/main.py
Normal 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
6
server/requirements.txt
Normal file
@ -0,0 +1,6 @@
|
||||
aiohttp
|
||||
fastapi
|
||||
jinja2
|
||||
Nuitka
|
||||
uvicorn
|
||||
websockets
|
1
server/static/scripts/color-thief.umd.js
Normal file
1
server/static/scripts/color-thief.umd.js
Normal file
File diff suppressed because one or more lines are too long
41
server/static/scripts/main.js
Normal file
41
server/static/scripts/main.js
Normal 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();
|
130
server/static/styles/main.css
Normal file
130
server/static/styles/main.css
Normal 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;
|
||||
}
|
30
server/templates/overlay.jinja2
Normal file
30
server/templates/overlay.jinja2
Normal 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>
|
Reference in New Issue
Block a user