fourth commit
This commit is contained in:
parent
3282d38821
commit
ab6324bf32
|
@ -3,7 +3,7 @@ A music streaming application.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
Python 3.8+
|
Python 3.8+
|
||||||
Python packages: `gunicorn aiohttp aiohttp_jinja2 uvloop mutagen`
|
Python packages: `gunicorn aiohttp aiohttp_jinja2 uvloop mutagen asyncpg`
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
```
|
```
|
||||||
|
|
22
events.py
Normal file
22
events.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
WebSocket events.
|
||||||
|
"""
|
||||||
|
import types
|
||||||
|
|
||||||
|
async def select(request, ws, data):
|
||||||
|
"""Retrieve information about an artist, album or track."""
|
||||||
|
if data.get('type') == 'artist':
|
||||||
|
async with request.app['pool'].acquire() as conn:
|
||||||
|
albums = await conn.fetch(
|
||||||
|
"SELECT DISTINCT album, date FROM track "
|
||||||
|
"WHERE albumartist = $1 ORDER BY date ASC",
|
||||||
|
data.get('artist', ''))
|
||||||
|
albums = [record['album'] for record in albums]
|
||||||
|
ret = {'event': 'albums', 'ok': True, 'data': albums}
|
||||||
|
await ws.send_json(ret)
|
||||||
|
|
||||||
|
events = {}
|
||||||
|
for obj in dir():
|
||||||
|
if type(locals()[obj]) == types.FunctionType:
|
||||||
|
events[locals()[obj].__name__] = locals()[obj]
|
18
scorch.py
18
scorch.py
|
@ -2,6 +2,7 @@
|
||||||
"""
|
"""
|
||||||
A music streaming application.
|
A music streaming application.
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
@ -13,6 +14,7 @@ import uvloop
|
||||||
import asyncpg
|
import asyncpg
|
||||||
|
|
||||||
import config
|
import config
|
||||||
|
import events
|
||||||
import database
|
import database
|
||||||
import buckler_aiohttp
|
import buckler_aiohttp
|
||||||
|
|
||||||
|
@ -23,9 +25,10 @@ routes = web.RouteTableDef()
|
||||||
async def index(request):
|
async def index(request):
|
||||||
"""The index page."""
|
"""The index page."""
|
||||||
async with request.app['pool'].acquire() as conn:
|
async with request.app['pool'].acquire() as conn:
|
||||||
artists = await conn.execute(
|
artists = await conn.fetch(
|
||||||
"SELECT DISTINCT albumartist FROM track ORDER BY albumartist ASC")
|
"SELECT DISTINCT albumartist FROM track ORDER BY albumartist ASC")
|
||||||
return render_template('index.html', request, {})
|
artists = [record['albumartist'] for record in artists]
|
||||||
|
return render_template('index.html', request, locals())
|
||||||
|
|
||||||
|
|
||||||
@routes.get('/ws', name='ws')
|
@routes.get('/ws', name='ws')
|
||||||
|
@ -41,10 +44,15 @@ async def websocket_handler(request):
|
||||||
if msg.type != WSMsgType.TEXT:
|
if msg.type != WSMsgType.TEXT:
|
||||||
break
|
break
|
||||||
|
|
||||||
if msg.data == "ping":
|
try:
|
||||||
print('ping')
|
data = json.loads(msg.data)
|
||||||
await ws.send_str("pong")
|
except json.JSONDecodeError:
|
||||||
|
break
|
||||||
|
|
||||||
|
event = data.get('event')
|
||||||
|
if not event or event not in events.events.keys():
|
||||||
|
break
|
||||||
|
await events.events[event](request, ws, data.get('data'))
|
||||||
await ws.close()
|
await ws.close()
|
||||||
return ws
|
return ws
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"h h h"
|
||||||
|
"a b c"
|
||||||
|
"f f f";
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
grid-area: h;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#artistListContainer {
|
||||||
|
grid-area: a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#albumListContainer {
|
||||||
|
grid-area: b;
|
||||||
|
}
|
||||||
|
|
||||||
|
#trackListContainer {
|
||||||
|
grid-area: c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playerContainer {
|
||||||
|
grid-area: f;
|
||||||
|
height: auto;
|
||||||
|
max-height: 30vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
|
@ -12,31 +12,90 @@ function init_websocket() {
|
||||||
socket.onclose = onclose;
|
socket.onclose = onclose;
|
||||||
socket.onerror = onerror;
|
socket.onerror = onerror;
|
||||||
socket.events = {};
|
socket.events = {};
|
||||||
|
socket.events['albums'] = albums_recv;
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
function send_event(event_title, data) {
|
function send_event(event_title, data) {
|
||||||
data = JSON.stringify({'event': event_title, 'data': data});
|
data = JSON.stringify({'event': event_title, 'data': data});
|
||||||
if (socket.readyState == 0) {
|
if (this.readyState == 0) {
|
||||||
console.log("Socket is still opening!");
|
console.log("Socket is still opening!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
socket.send(data);
|
this.send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onmessage(e) {
|
function onmessage(event) {
|
||||||
console.log(e);
|
let data;
|
||||||
|
let event_title;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(event.data);
|
||||||
|
} catch(err) {
|
||||||
|
// not JSON
|
||||||
|
console.log(err);
|
||||||
|
console.log(event);
|
||||||
|
throw new Error("Error decoding JSON");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onclose(e) {
|
if (!data.ok) {
|
||||||
if (e.wasClean) { return; } // no need to reconnect
|
throw new Error("Socket error: event = " + event_title + ", error = " + data.error);
|
||||||
console.log(e);
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
event_title = data.event;
|
||||||
|
data = data.data;
|
||||||
|
} catch(err) {
|
||||||
|
// not proper event
|
||||||
|
console.log(err);
|
||||||
|
console.log(event);
|
||||||
|
throw new Error("Event malformed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socket.events[event_title] === undefined) {
|
||||||
|
console.log("Unknown socket event: " + event_title);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socket.events[event_title](data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onclose(event) {
|
||||||
|
if (event.wasClean) { return; } // no need to reconnect
|
||||||
|
console.log(event);
|
||||||
console.log('Websocket lost connection to server. Re-trying...');
|
console.log('Websocket lost connection to server. Re-trying...');
|
||||||
//socket = init_websocket();
|
await sleep(3000);
|
||||||
//await sleep(5000);
|
socket = init_websocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onerror(e) {
|
function sleep(ms) {
|
||||||
console.log("Websocket error!")
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
console.log(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onerror(event) {
|
||||||
|
console.log("Websocket error!")
|
||||||
|
console.log(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Websocket receive */
|
||||||
|
function albums_recv(data) {
|
||||||
|
let albums_list = document.querySelector('#albumList');
|
||||||
|
while (albums_list.firstChild) {
|
||||||
|
albums_list.removeChild(albums_list.lastChild);
|
||||||
|
}
|
||||||
|
for (let album of data) {
|
||||||
|
let option = document.createElement('option');
|
||||||
|
option.value = album;
|
||||||
|
option.innerText = album;
|
||||||
|
albums_list.appendChild(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Websocket send */
|
||||||
|
function select_artist(option) {
|
||||||
|
let artist = option.value;
|
||||||
|
let data = {'type': 'artist', 'artist': artist};
|
||||||
|
socket.send_event('select', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DOM */
|
||||||
|
|
|
@ -7,15 +7,42 @@
|
||||||
<script type="text/javascript" src="/static/scorch.js"></script>
|
<script type="text/javascript" src="/static/scorch.js"></script>
|
||||||
<script>window.onload = load;</script>
|
<script>window.onload = load;</script>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||||
<meta name="description" content="A music streaming service.">
|
<meta name="description" content="A music streaming platform.">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
|
||||||
<h1>Scorch</h1>
|
|
||||||
</header>
|
|
||||||
<main>
|
<main>
|
||||||
<button onclick="socket.send('test')">Test</button>
|
<div id="header">
|
||||||
<audio controls></audio>
|
<h1>Scorch</h1>
|
||||||
|
</div>
|
||||||
|
<div id="artistListContainer" class="list">
|
||||||
|
<select id="artistList" size="2" class="list" onchange="select_artist(this)">
|
||||||
|
{% for artist in artists %}
|
||||||
|
<option value="{{ artist }}">{{ artist }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="albumListContainer" class="list">
|
||||||
|
<select id="albumList" size="2" class="list" onchange="select_album(this)">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="trackListContainer" class="list">
|
||||||
|
<select id="trackList" size="2" class="list" onchange="select_track(this)">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="playerContainer">
|
||||||
|
<span id="albumCover"><img src=""></span>
|
||||||
|
<span id="playerControls">
|
||||||
|
<h4><span id="nowPlayingArtist"></span> - <span id="nowPlayingAlbum"></span></h4>
|
||||||
|
<h3 id="nowPlayingTitle"></h3>
|
||||||
|
<audio id="player" controls>
|
||||||
|
<source id="stream" src="" type="audio/ogg">
|
||||||
|
</audio>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" name="shuffle" id="shuffle" checked="true">
|
||||||
|
<label for="shuffle">Shuffle</label>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user