first commit
This commit is contained in:
commit
9bfe8e0996
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
__pycache__/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
config.py
|
15
LICENSE
Normal file
15
LICENSE
Normal 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
20
README.md
Normal 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
74
buckler_fastapi.py
Normal 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
28
config.py.template
Normal 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
79
pyrite.py
Normal 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)
|
13
static/pyrite-manifest.json
Normal file
13
static/pyrite-manifest.json
Normal 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
71
static/pyrite.js
Normal 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
15
templates/index.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user