Add pl8list infinite-scroll app with pl8list sign-in

Endless TikTok-style feed over pl8list's Remix loaders. A guest crawler
builds a shared catalogue; a per-user ranker learns from likes, full
listens and skips. Users sign in with pl8list's email code; only a hash
of the local session token is stored. Ships as a hardened Docker image
with a Traefik override for pl8.narl.io.
This commit is contained in:
nvrl committed 2026-10-31 20:22:27 +01:00
commit 4a7f6903a3
20 files changed
+2192

No files matched your search

+196
View File
@@ -0,0 +1,196 @@
"""SQLite store: the shared track catalogue, plus per-user accounts and listening events."""
import hashlib
import secrets
import sqlite3
import threading
import time
SESSION_DAYS = 90
SCHEMA = """
CREATE TABLE IF NOT EXISTS artists (
handle TEXT PRIMARY KEY,
name TEXT,
img TEXT,
verified INTEGER DEFAULT 0,
source TEXT,
pages_fetched INTEGER DEFAULT 0,
total_pages INTEGER,
last_crawled REAL
);
CREATE TABLE IF NOT EXISTS tracks (
id INTEGER PRIMARY KEY,
handle TEXT NOT NULL,
artist_name TEXT,
artist_img TEXT,
artist_verified INTEGER DEFAULT 0,
slug TEXT NOT NULL,
title TEXT,
description TEXT,
bpm REAL,
key TEXT,
duration REAL,
preview_duration REAL,
price INTEGER DEFAULT 0,
created_at TEXT,
upload_id TEXT,
preview_upload_id TEXT,
waveform TEXT,
preview_waveform TEXT,
full_access INTEGER DEFAULT 0,
saves INTEGER DEFAULT 0,
downloads INTEGER DEFAULT 0,
purchases INTEGER DEFAULT 0,
comments INTEGER DEFAULT 0,
related_of INTEGER,
related_fetched INTEGER DEFAULT 0,
updated_at REAL
);
CREATE INDEX IF NOT EXISTS tracks_handle ON tracks(handle);
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
track_id INTEGER NOT NULL,
kind TEXT NOT NULL,
seconds REAL,
ts REAL NOT NULL
);
-- Users are pl8list accounts, keyed by their pl8list user id. No pl8list session is stored.
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
handle TEXT NOT NULL,
name TEXT,
img TEXT,
created_at REAL NOT NULL,
last_seen REAL
);
-- Our own login sessions. Only a hash of the cookie token is kept.
CREATE TABLE IF NOT EXISTS sessions (
token_hash TEXT PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at REAL NOT NULL,
expires_at REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS search_terms (
term TEXT PRIMARY KEY,
pages_fetched INTEGER DEFAULT 0,
total_pages INTEGER
);
"""
TRACK_COLS = [
"id", "handle", "artist_name", "artist_img", "artist_verified", "slug", "title", "description",
"bpm", "key", "duration", "preview_duration", "price", "created_at", "upload_id",
"preview_upload_id", "waveform", "preview_waveform", "full_access", "saves", "downloads",
"purchases", "comments",
]
class DB:
def __init__(self, path):
self.conn = sqlite3.connect(path, check_same_thread=False, isolation_level=None)
self.conn.row_factory = sqlite3.Row
self.conn.execute("PRAGMA journal_mode=WAL")
self.conn.execute("PRAGMA foreign_keys=ON")
self.conn.executescript(SCHEMA)
# Databases from the single-user version have events without an owner.
if "user_id" not in [r["name"] for r in self.conn.execute("PRAGMA table_info(events)")]:
self.conn.execute("DELETE FROM events")
self.conn.execute("ALTER TABLE events ADD COLUMN user_id INTEGER")
self.conn.execute("CREATE INDEX IF NOT EXISTS events_user ON events(user_id, kind, track_id)")
self.lock = threading.Lock()
def query(self, sql, args=()):
with self.lock:
return self.conn.execute(sql, args).fetchall()
def execute(self, sql, args=()):
with self.lock:
return self.conn.execute(sql, args)
def upsert_tracks(self, rows, related_of=None):
"""Insert or refresh tracks. Returns how many were new."""
new = 0
now = time.time()
with self.lock:
for r in rows:
if not r:
continue
exists = self.conn.execute("SELECT 1 FROM tracks WHERE id=?", (r["id"],)).fetchone()
vals = [r[c] for c in TRACK_COLS]
if exists:
sets = ",".join("%s=?" % c for c in TRACK_COLS[1:])
self.conn.execute("UPDATE tracks SET %s, updated_at=? WHERE id=?" % sets,
vals[1:] + [now, r["id"]])
else:
new += 1
self.conn.execute(
"INSERT INTO tracks (%s, related_of, updated_at) VALUES (%s)"
% (",".join(TRACK_COLS), ",".join("?" * (len(TRACK_COLS) + 2))),
vals + [related_of, now])
self.conn.execute(
"INSERT INTO artists (handle, name, img, verified, source) VALUES (?,?,?,?,?) "
"ON CONFLICT(handle) DO UPDATE SET name=excluded.name, img=excluded.img, "
"verified=excluded.verified",
(r["handle"], r["artist_name"], r["artist_img"], r["artist_verified"], "track"))
return new
def upsert_artist(self, handle, name=None, img=None, verified=0, source=None):
self.execute(
"INSERT INTO artists (handle, name, img, verified, source) VALUES (?,?,?,?,?) "
"ON CONFLICT(handle) DO UPDATE SET name=COALESCE(excluded.name, name), "
"img=COALESCE(excluded.img, img), verified=MAX(verified, excluded.verified)",
(handle, name, img, int(bool(verified)), source))
def add_event(self, user_id, track_id, kind, seconds=None):
self.execute("INSERT INTO events (user_id, track_id, kind, seconds, ts) VALUES (?,?,?,?,?)",
(user_id, track_id, kind, seconds, time.time()))
def stats(self, user_id):
row = self.query("""
SELECT (SELECT COUNT(*) FROM tracks) AS tracks,
(SELECT COUNT(*) FROM artists) AS artists,
(SELECT COUNT(*) FROM artists WHERE pages_fetched > 0) AS artists_crawled,
(SELECT COUNT(DISTINCT track_id) FROM events WHERE user_id=? AND kind='served') AS served
""", (user_id,))[0]
return dict(row)
# Accounts --------------------------------------------------------------
def upsert_user(self, user):
now = time.time()
self.execute(
"INSERT INTO users (id, handle, name, img, created_at, last_seen) VALUES (?,?,?,?,?,?) "
"ON CONFLICT(id) DO UPDATE SET handle=excluded.handle, name=excluded.name, img=excluded.img, "
"last_seen=excluded.last_seen",
(user["id"], user["handle"], user.get("name"), user.get("img"), now, now))
def create_session(self, user_id):
"""Returns the raw token for the cookie; only its hash is stored."""
token = secrets.token_urlsafe(32)
now = time.time()
self.execute("INSERT INTO sessions (token_hash, user_id, created_at, expires_at) VALUES (?,?,?,?)",
(_hash(token), user_id, now, now + SESSION_DAYS * 86400))
return token
def session_user(self, token):
rows = self.query("""
SELECT u.* FROM sessions s JOIN users u ON u.id = s.user_id
WHERE s.token_hash=? AND s.expires_at > ?
""", (_hash(token), time.time()))
if not rows:
return None
user = dict(rows[0])
if (user["last_seen"] or 0) < time.time() - 60:
self.execute("UPDATE users SET last_seen=? WHERE id=?", (time.time(), user["id"]))
return user
def delete_session(self, token):
self.execute("DELETE FROM sessions WHERE token_hash=?", (_hash(token),))
def active_since(self, ts):
return self.query("SELECT COUNT(*) AS n FROM users WHERE last_seen > ?", (ts,))[0]["n"]
def _hash(token):
return hashlib.sha256(token.encode()).hexdigest()