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