diff --git a/README.md b/README.md index c9b4636..51d871b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A music streaming application. ## Requirements Python 3.8+ -Python packages: `gunicorn aiohttp aiohttp_jinja2 uvloop mutagen` +Python packages: `gunicorn aiohttp aiohttp_jinja2 uvloop mutagen asyncpg` ## Install ``` diff --git a/events.py b/events.py new file mode 100644 index 0000000..6c1d1aa --- /dev/null +++ b/events.py @@ -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] diff --git a/scorch.py b/scorch.py index 3e73a6c..ab4b86a 100644 --- a/scorch.py +++ b/scorch.py @@ -2,6 +2,7 @@ """ A music streaming application. """ +import json import asyncio import aiohttp @@ -13,6 +14,7 @@ import uvloop import asyncpg import config +import events import database import buckler_aiohttp @@ -23,9 +25,10 @@ routes = web.RouteTableDef() async def index(request): """The index page.""" 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") - 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') @@ -41,10 +44,15 @@ async def websocket_handler(request): if msg.type != WSMsgType.TEXT: break - if msg.data == "ping": - print('ping') - await ws.send_str("pong") + try: + data = json.loads(msg.data) + 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() return ws diff --git a/static/scorch.css b/static/scorch.css index e69de29..cc54455 100644 --- a/static/scorch.css +++ b/static/scorch.css @@ -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; +} diff --git a/static/scorch.js b/static/scorch.js index 0865802..f9e0698 100644 --- a/static/scorch.js +++ b/static/scorch.js @@ -12,31 +12,90 @@ function init_websocket() { socket.onclose = onclose; socket.onerror = onerror; socket.events = {}; + socket.events['albums'] = albums_recv; return socket; } function send_event(event_title, data) { data = JSON.stringify({'event': event_title, 'data': data}); - if (socket.readyState == 0) { + if (this.readyState == 0) { console.log("Socket is still opening!"); return; } - socket.send(data); + this.send(data); } -function onmessage(e) { - console.log(e); +function onmessage(event) { + 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; + } + + if (!data.ok) { + throw new Error("Socket error: event = " + event_title + ", error = " + data.error); + } + + 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); } -function onclose(e) { - if (e.wasClean) { return; } // no need to reconnect - console.log(e); +async function onclose(event) { + if (event.wasClean) { return; } // no need to reconnect + console.log(event); console.log('Websocket lost connection to server. Re-trying...'); - //socket = init_websocket(); - //await sleep(5000); + await sleep(3000); + socket = init_websocket(); } -function onerror(e) { - console.log("Websocket error!") - console.log(e); +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); } + +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 */ diff --git a/templates/index.html b/templates/index.html index bcda2f8..f4f27b6 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,15 +7,42 @@ - + -
-

Scorch

-
- - + +
+ +
+
+ +
+
+ +
+
+ + +

-

+

+ +
+ + +
+
+