From 53e102c8b22e9739ad39fe72cb63a0c45eaec320 Mon Sep 17 00:00:00 2001 From: evilchili Date: Fri, 2 Dec 2022 21:43:51 -0800 Subject: [PATCH] wip web playback --- groove/cli.py | 2 +- groove/exceptions.py | 5 +++ groove/playlist.py | 9 ++++- groove/webserver/__init__.py | 0 groove/webserver/requests.py | 39 +++++++++++++++++++ groove/{ => webserver}/webserver.py | 58 +++++++++++++++++++++++------ web-templates/playlist.tpl | 6 +++ 7 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 groove/exceptions.py create mode 100644 groove/webserver/__init__.py create mode 100644 groove/webserver/requests.py rename groove/{ => webserver}/webserver.py (51%) create mode 100644 web-templates/playlist.tpl diff --git a/groove/cli.py b/groove/cli.py index f874bad..bc5746f 100644 --- a/groove/cli.py +++ b/groove/cli.py @@ -9,12 +9,12 @@ from slugify import slugify from rich import print import rich.table -from groove import webserver from groove.shell import interactive_shell from groove.playlist import Playlist from groove import db from groove.db.manager import database_manager from groove.db.scanner import media_scanner +from groove.webserver import webserver playlist_app = typer.Typer() app = typer.Typer() diff --git a/groove/exceptions.py b/groove/exceptions.py new file mode 100644 index 0000000..a21ed70 --- /dev/null +++ b/groove/exceptions.py @@ -0,0 +1,5 @@ + +class APIHandlingException(Exception): + """ + An API reqeust could not be encoded or decoded. + """ diff --git a/groove/playlist.py b/groove/playlist.py index 84ab4ff..6114a09 100644 --- a/groove/playlist.py +++ b/groove/playlist.py @@ -35,9 +35,12 @@ class Playlist: @property def exists(self) -> bool: if self.deleted: + logging.debug("Playlist has been deleted.") return False if not self._record: - return (self._create_ok and self.record) + if self._create_ok: + return True and self.record + return False return True @property @@ -163,6 +166,10 @@ class Playlist: if self._create_ok or create_ok: return self.save() + def load(self): + self.get_or_create(create_ok=False) + return self + def save(self) -> Row: keys = {'slug': self.slug, 'name': self._name, 'description': self._description} stmt = db.playlist.update(keys) if self._record else db.playlist.insert(keys) diff --git a/groove/webserver/__init__.py b/groove/webserver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/groove/webserver/requests.py b/groove/webserver/requests.py new file mode 100644 index 0000000..6a2b06c --- /dev/null +++ b/groove/webserver/requests.py @@ -0,0 +1,39 @@ +from hashlib import blake2b +from hmac import compare_digest +from typing import List +import os + + +def encode(args: List, uri: str) -> str: + """ + Encode a request and cryptographically sign it. This serves two purposes: + First, it enables the handler that receives the request to verify that the + request was meant for it, preventing various routing and relay-induced bugs. + Second, it ensures the request wasn't corrutped or tampered with during + transmission. + + Args: + args (List): a list of parameters to pass along with the request. + uri (String): the URI of the intended handler for the request. + + Returns: + String: A cryptographically signed request. + """ + return sign(uri + '\0' + '\0'.join(args)) + + +def sign(request): + """ + Sign a request with a cryptographic hash. Returns the hex digest. + """ + h = blake2b(digest_size=16, key=bytes(os.environ['SECRET_KEY'].encode())) + h.update(request.encode()) + return h.hexdigest() + + +def verify(request, digest): + return compare_digest(request, digest) + + +def url(): + return f"http://{os.environ['HOST']}:{os.environ['PORT']}" diff --git a/groove/webserver.py b/groove/webserver/webserver.py similarity index 51% rename from groove/webserver.py rename to groove/webserver/webserver.py index 7fbc197..72d1ec9 100644 --- a/groove/webserver.py +++ b/groove/webserver/webserver.py @@ -1,15 +1,19 @@ -import json import logging +import json import os +from pathlib import Path import bottle -from bottle import HTTPResponse +from bottle import HTTPResponse, template, static_file from bottle.ext import sqlalchemy +import groove.db from groove.auth import is_authenticated from groove.db.manager import database_manager -from groove.db import metadata from groove.playlist import Playlist +from groove.webserver import requests + +from groove.exceptions import APIHandlingException server = bottle.Bottle() @@ -23,7 +27,7 @@ def start(host: str, port: int, debug: bool) -> None: # pragma: no cover with database_manager() as manager: server.install(sqlalchemy.Plugin( manager.engine, - metadata, + groove.db.metadata, keyword='db', create=True, commit=True, @@ -49,16 +53,48 @@ def build(): return "Authenticated. Groovy." +@bottle.auth_basic(is_authenticated) +@server.route('/build/search/playlist') +def search_playlist(slug, db): + playlist = Playlist(slug=slug, session=db, create_ok=False) + response = json.dumps(playlist.as_dict) + logging.debug(response) + return HTTPResponse(status=200, content_type='application/json', body=response) + + +@server.route('/track//') +def serve_track(request, track_id, db): + + expected = requests.encode([track_id], '/track') + if not requests.verify(request, expected): + return HTTPResponse(status=404, body="Not found") + + track_id = int(track_id) + track = db.query(groove.db.track).filter( + groove.db.track.c.id == track_id + ).one() + + path = Path(os.environ['MEDIA_ROOT']) / Path(track['relpath']) + return static_file(path.name, root=path.parent) + + @server.route('/playlist/') -def get_playlist(slug, db): +def serve_playlist(slug, db): """ Retrieve a playlist and its entries by a slug. """ logging.debug(f"Looking up playlist: {slug}...") - playlist = Playlist(slug=slug, session=db, create_ok=False) - print(playlist.record) - if not playlist.exists: + playlist = Playlist(slug=slug, session=db, create_ok=False).load() + if not playlist.record: + logging.debug(f"Playist {slug} doesn't exist.") return HTTPResponse(status=404, body="Not found") - response = json.dumps(playlist.as_dict) - logging.debug(response) - return HTTPResponse(status=200, content_type='application/json', body=response) + logging.debug(f"Loaded {playlist.record}") + logging.debug(playlist.as_dict['entries']) + + args = [ + (requests.encode([str(entry['track_id'])], uri='/track'), entry['track_id']) + for entry in playlist.as_dict['entries'] + ] + + template_path = Path(os.environ['TEMPLATE_PATH']) / Path('playlist.tpl') + return template(str(template_path), url=requests.url(), playlist=playlist.as_dict, args=args) diff --git a/web-templates/playlist.tpl b/web-templates/playlist.tpl new file mode 100644 index 0000000..d876aea --- /dev/null +++ b/web-templates/playlist.tpl @@ -0,0 +1,6 @@ +