first commit

This commit is contained in:
iou1name 2024-10-29 10:53:46 -04:00
commit 9bfe8e0996
9 changed files with 319 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
__pycache__/
*.swp
*.swo
config.py

15
LICENSE Normal file
View File

@ -0,0 +1,15 @@
ISC License
Copyright (c) 2024, iou1name <iou1name@steelbea.me>
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.

20
README.md Normal file
View File

@ -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`

74
buckler_fastapi.py Normal file
View File

@ -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

28
config.py.template Normal file
View File

@ -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",
}

79
pyrite.py Normal file
View File

@ -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)

View File

@ -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"
]
}

71
static/pyrite.js Normal file
View File

@ -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)
});
}

15
templates/index.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Pyrite</title>
<script type="text/javascript" src="./static/pyrite.js"></script>
<script>window.onload = init;</script>
</head>
<body>
<div>
<audio id="player" controls></audio>
<button id="play-btn" onclick="playTrack()">Play</button>
<button id="pause-btn" onclick="pauseTrack()">Pause</button>
</div>
</body>
</html>