From 9bfe8e0996c8623d8d21efb3158e1e48c64b631b Mon Sep 17 00:00:00 2001 From: iou1name Date: Tue, 29 Oct 2024 10:53:46 -0400 Subject: [PATCH] first commit --- .gitignore | 4 ++ LICENSE | 15 +++++++ README.md | 20 ++++++++++ buckler_fastapi.py | 74 ++++++++++++++++++++++++++++++++++ config.py.template | 28 +++++++++++++ pyrite.py | 79 +++++++++++++++++++++++++++++++++++++ static/pyrite-manifest.json | 13 ++++++ static/pyrite.js | 71 +++++++++++++++++++++++++++++++++ templates/index.html | 15 +++++++ 9 files changed, 319 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 buckler_fastapi.py create mode 100644 config.py.template create mode 100644 pyrite.py create mode 100644 static/pyrite-manifest.json create mode 100644 static/pyrite.js create mode 100644 templates/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..610faed --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.swp +*.swo +config.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f2c0782 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2024, iou1name + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..95281f1 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Pyrite +A naturally occurring iron disulfide mineral. The name comes from the Greek word pyr, “fire,” because pyrite emits sparks when struck by metal. Pyrite is called fool's gold; to the novice its colour is deceptively similar to that of a gold nugget. + +## Requirements +Python 3.12+ +Python packages: `fastapi uvicorn[standard] httpx jinja2 asyncpg mutagen` + +## Install +``` +$ psql +postgres=# CREATE DATABASE "pyrite"; +postgres=# CREATE USER "pyrite" WITH PASSWORD 'password'; +postgres=# GRANT ALL PRIVILEGES ON DATABASE "pyrite" TO "pyrite"; +postgres=# \q +``` +1. Get on the floor +2. Walk the dinosaur + +## Usage +`uvicorn pyrite:app --host localhost --port 5650` diff --git a/buckler_fastapi.py b/buckler_fastapi.py new file mode 100644 index 0000000..c2fe47c --- /dev/null +++ b/buckler_fastapi.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +Session interface middlewares to integrate the fastapi app with Buckler. +""" +import json +from datetime import datetime + +import httpx +from fastapi import FastAPI, Request +from fastapi.responses import RedirectResponse +from starlette.middleware.base import BaseHTTPMiddleware + +import config + + +class BucklerSessionMiddleware(BaseHTTPMiddleware): + """ + Verifies the user with the configured Buckler app and retrieves any + session data they may have. Redirects them to the login page otherwise. + """ + def __init__(self, app): + super().__init__(app) + + async def dispatch(self, request: Request, call_next): + user_id = request.cookies.get('userid', '') + user_sid = request.cookies.get('session', '') + + url = config.buckler['url'] + '/get_session' + params = { + 'app_id': config.buckler['app_id'], + 'app_key': config.buckler['app_key'], + 'userid': user_id, + 'session': user_sid } + + async with httpx.AsyncClient() as client: + resp = await client.get(url, params=params) + data = resp.json() + + if data.get('error'): # user not logged in + response = RedirectResponse(config.buckler['login_url']) + response.set_cookie('redirect', config.server_homepage) + return response + + request.state.session = data.get('session_data') + request.state.meta = data.get('meta') + response = await call_next(request) + + if request.state.session != data['session_data']: # session data modified + url = config.buckler['url'] + '/set_session' + data = json.dumps(request.state.session) + async with httpx.AsyncClient() as client: + await client.post(url, params=params, data=data) + + last_used = datetime.fromisoformat(request.state.meta['last_used']) + now = datetime.now(last_used.tzinfo) + delta = now - last_used + if delta.seconds > 600: + response.set_cookie( + 'userid', + user_id, + domain=config.server_domain, + max_age=30*24*60*60, + secure=True, + httponly=True, + samesite='strict') + response.set_cookie( + 'session', + user_sid, + domain=config.server_domain, + max_age=30*24*60*60, + secure=True, + httponly=True, + samesite='strict') + return response diff --git a/config.py.template b/config.py.template new file mode 100644 index 0000000..4de979a --- /dev/null +++ b/config.py.template @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +""" +Config settings for Pyrite. + +`db` specifies settings for connecting to the database server. +`music_dir` the directory containing music files being served. +`server_domain` the root domain of the website, used for setting cookies. +`server_homepage` the app's homepage, used for Buckler redirects. +`buckler` specifies settings pertaining to the Buckler server. +""" +db = { + 'database': 'pyrite', + 'user': 'pyrite', + 'password': r"""password""", + 'host': 'localhost', + 'port': 5432, +} + +music_dir = "/home/iou1name/music/opus/" + +server_domain = 'steelbea.me' +server_homepage = 'https://steelbea.me/pyrite' +buckler = { + 'url': "http://127.0.0.1:5400/buckler", + 'app_id': 0, + 'app_key': r"""password""", + 'login_url': "/buckler/login", +} diff --git a/pyrite.py b/pyrite.py new file mode 100644 index 0000000..f902698 --- /dev/null +++ b/pyrite.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +""" +A music steaming application. +""" +import random + +from fastapi import FastAPI, Request +from fastapi.templating import Jinja2Templates +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse + +#import buckler_fastapi + +app = FastAPI() +#app.add_middleware(buckler_fastapi.BucklerSessionMiddleware) + +app.mount("/static", StaticFiles(directory="static"), name="static") +templates = Jinja2Templates(directory="templates") + +@app.get("/", response_class=HTMLResponse) +async def index(request: Request): + context = {"request": request} + return templates.TemplateResponse('index.html', context) + +@app.get("/rand_track/") +async def get_rand_track(): + """Return a random track.""" + tracks = [{ + 'title': "Blade Runner", + 'artist': "Beast In Black", + 'album': "Dark Connection", + 'source': "https://steelbea.me/scorch_test/library/Beast%20In%20Black/Beast%20In%20Black%20-%20Dark%20Connection%20%282021%29%20%5BFLAC%5D%20%7BNB62072%7D/01%20-%20Blade%20Runner.opus", + 'artwork': [{ + 'src': "https://steelbea.me/scorch_test/library/Beast%20In%20Black/Beast%20In%20Black%20-%20Dark%20Connection%20%282021%29%20%5BFLAC%5D%20%7BNB62072%7D/folder-256x256.jpg", + 'sizes': "256x256", + 'type': "image/jpeg" + }] + }, { + 'title': "Bella Donna", + 'artist': "Beast In Black", + 'album': "Dark Connection", + 'source': "https://steelbea.me/scorch_test/library/Beast%20In%20Black/Beast%20In%20Black%20-%20Dark%20Connection%20%282021%29%20%5BFLAC%5D%20%7BNB62072%7D/02%20-%20Bella%20Donna.opus", + 'artwork': [{ + 'src': "https://steelbea.me/scorch_test/library/Beast%20In%20Black/Beast%20In%20Black%20-%20Dark%20Connection%20%282021%29%20%5BFLAC%5D%20%7BNB62072%7D/folder-256x256.jpg", + 'sizes': "256x256", + 'type': "image/jpeg" + }] + }, { + 'title': "Highway To Mars", + 'artist': "Beast In Black", + 'album': "Dark Connection", + 'source': "https://steelbea.me/scorch_test/library/Beast%20In%20Black/Beast%20In%20Black%20-%20Dark%20Connection%20%282021%29%20%5BFLAC%5D%20%7BNB62072%7D/03%20-%20Highway%20To%20Mars.opus", + 'artwork': [{ + 'src': "https://steelbea.me/scorch_test/library/Beast%20In%20Black/Beast%20In%20Black%20-%20Dark%20Connection%20%282021%29%20%5BFLAC%5D%20%7BNB62072%7D/folder-256x256.jpg", + 'sizes': "256x256", + 'type': "image/jpeg" + }] + }, { + 'title': "Hardcore", + 'artist': "Beast In Black", + 'album': "Dark Connection", + 'source': "https://steelbea.me/scorch_test/library/Beast%20In%20Black/Beast%20In%20Black%20-%20Dark%20Connection%20%282021%29%20%5BFLAC%5D%20%7BNB62072%7D/04%20-%20Hardcore.opus", + 'artwork': [{ + 'src': "https://steelbea.me/scorch_test/library/Beast%20In%20Black/Beast%20In%20Black%20-%20Dark%20Connection%20%282021%29%20%5BFLAC%5D%20%7BNB62072%7D/folder-256x256.jpg", + 'sizes': "256x256", + 'type': "image/jpeg" + }] + }, { + 'title': "One Night In Tokyo", + 'artist': "Beast In Black", + 'album': "Dark Connection", + 'source': "https://steelbea.me/scorch_test/library/Beast%20In%20Black/Beast%20In%20Black%20-%20Dark%20Connection%20%282021%29%20%5BFLAC%5D%20%7BNB62072%7D/05%20-%20One%20Night%20In%20Tokyo.opus", + 'artwork': [{ + 'src': "https://steelbea.me/scorch_test/library/Beast%20In%20Black/Beast%20In%20Black%20-%20Dark%20Connection%20%282021%29%20%5BFLAC%5D%20%7BNB62072%7D/folder-256x256.jpg", + 'sizes': "256x256", + 'type': "image/jpeg" + }] + }] + return random.choice(tracks) diff --git a/static/pyrite-manifest.json b/static/pyrite-manifest.json new file mode 100644 index 0000000..9c4bf9a --- /dev/null +++ b/static/pyrite-manifest.json @@ -0,0 +1,13 @@ +{ + "name": "Pyrite", + "short_name": "Pyrite", + "start_url": "/pyrite", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#000000", + "description": "A music streaming application.", + "icons": [ + "src": "/favicon.ico", + "sizes": "16x16" + ] +} diff --git a/static/pyrite.js b/static/pyrite.js new file mode 100644 index 0000000..3ef703e --- /dev/null +++ b/static/pyrite.js @@ -0,0 +1,71 @@ +var player; +var pos_int; + +function init() { + player = document.getElementById('player'); + + player.addEventListener("ended", nextTrack); + + navigator.mediaSession.setActionHandler('play', playTrack); + navigator.mediaSession.setActionHandler('pause', pauseTrack); + navigator.mediaSession.setActionHandler('stop', stopTrack); + navigator.mediaSession.setActionHandler('previoustrack', prevTrack); + navigator.mediaSession.setActionHandler('nexttrack', nextTrack); + + pos_int = setInterval(update_position, 300); +} + +async function get_rand_track() { + let res = await fetch("./rand_track/") + let data = await res.json(); + return data; +} + +async function nextTrack() { + track = await get_rand_track(); + player.src = track.source; + playTrack(); +} + +function prevTrack() { + // TODO: implement a history array +} + +async function playTrack() { + if (!player.src) { + return nextTrack(); + } + player.play(); + navigator.mediaSession.playbackState = "playing"; + setMediaSession(track); +} + +function pauseTrack() { + player.pause(); + navigator.mediaSession.playbackState = "paused"; +} + +function stopTrack() { + player.pause(); + player.load(); + navigator.mediaSession.playbackState = "none"; +} + +function setMediaSession(track) { + navigator.mediaSession.metadata = new MediaMetadata({ + title: track.title, + artist: track.artist, + album: track.album, + artwork: track.artwork + }); +} + +function update_position() { + if (player.paused){return;} + if (player.duration > 0 === false){return;} + navigator.mediaSession.setPositionState({ + duration: parseInt(player.duration), + playbackRate: player.playbackRate, + position: parseInt(player.currentTime) + }); +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..7f35c46 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,15 @@ + + + + Pyrite + + + + +
+ + + +
+ +