Audiobooks: built-in library, playback and synced progress through the audiobook player (classic frame kept)
build-container / build (push) Canceled after 2m57s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-22 14:48:05 -04:00
1 parent 66a376d22c
commit 62ff4d815d
14 files changed
+255 -15

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
// Audiobooks through the hub: the hub signs in to the player as the person, streams audio with ranges, keeps progress per person, and blocks everything else.
const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path');
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-ab-'));
process.env.DATA_DIR = dir; process.env.COOKIE_SECRET = 'ab-'.repeat(12);
const seen = []; let progress = {};
const player = http.createServer((req, res) => {
let b = ''; req.on('data', c => b += c); req.on('end', () => {
seen.push({ m: req.method, u: req.url, cookie: req.headers.cookie, range: req.headers.range });
const j = (c, o, h = {}) => { res.writeHead(c, { 'content-type': 'application/json', ...h }); res.end(JSON.stringify(o)); };
if (req.url === '/login') { const o = JSON.parse(b); return o.username === 'nate' && o.password === 'pw' ? j(200, { user: 'nate' }, { 'set-cookie': 'oa_session=tok1; Path=/; HttpOnly' }) : j(401, { error: 'wrong' }); }
if (!/oa_session=tok1/.test(req.headers.cookie || '')) return j(401, { error: 'not signed in' });
if (req.url === '/api/books') return j(200, [{ id: 'b1', title: 'Book One', author: 'A. Writer' }]);
if (req.url === '/admin/api/books/b1/files') return j(200, ['b1.m4b', 'b1.mp3']);
if (req.url === '/api/books/b1/files/b1.mp3') { res.writeHead(206, { 'content-type': 'audio/mpeg', 'content-range': 'bytes 0-3/10', 'content-length': '4' }); return res.end('abcd'); }
if (req.url === '/sync/progress' && req.method === 'POST') { progress = { ...progress, ...JSON.parse(b) }; return j(200, progress); }
if (req.url === '/sync/progress') return j(200, progress);
j(404, { error: 'no' });
});
}).listen(0, async () => {
process.env.AUDIOBOOKS_URL = 'http://127.0.0.1:' + player.address().port;
const users = require('../users'), abapi = require('../abapi');
const me = { id: 'ab-user', name: 'Nate' };
fs.mkdirSync(path.join(dir, 'users', me.id), { recursive: true });
fs.writeFileSync(path.join(dir, 'users', me.id, 'settings.json'), JSON.stringify({ ab_creds: { u: 'nate', p: 'pw' } }));
const hub = http.createServer((req, res) => users.runAs(req.headers['x-nobody'] ? { id: 'nobody', name: 'N' } : me, () => abapi.handle(req, res))).listen(0, async () => {
const base = 'http://127.0.0.1:' + hub.address().port, get = (p, o = {}) => fetch(base + '/api/ab' + p, { redirect: 'manual', ...o });
try {
let r = await get('/books'); assert.equal(r.status, 200); assert.equal((await r.json())[0].title, 'Book One');
assert.ok(/oa_session=tok1/.test(seen.at(-1).cookie), 'signed in as the person');
r = await get('/books/b1/filelist'); assert.deepEqual(await r.json(), ['b1.m4b', 'b1.mp3']);
r = await get('/books/b1/files/b1.mp3', { headers: { range: 'bytes=0-3' } }); assert.equal(r.status, 206); assert.equal(await r.text(), 'abcd'); assert.equal(seen.at(-1).range, 'bytes=0-3');
r = await get('/progress', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ b1: { pos: 12, dur: 100, t: 5 } }) }); assert.equal(r.status, 200);
r = await get('/progress'); assert.equal((await r.json()).b1.pos, 12);
const logins = seen.filter(s => s.u === '/login').length; assert.equal(logins, 1, 'one sign-in is reused');
const n = seen.length;
for (const [m, p] of [['POST', '/books'], ['GET', '/admin/users'], ['GET', '/books/b1'], ['GET', '/books/b1/files'], ['DELETE', '/progress'], ['GET', '/books/../admin']]) {
r = await get(p, { method: m }); assert.ok([404, 405].includes(r.status), m + ' ' + p + ' -> ' + r.status);
}
assert.equal(seen.length, n, 'blocked calls never reach the player');
r = await get('/books', { headers: { 'x-nobody': '1' } }); assert.equal(r.status, 503); assert.ok(/sign-in in Settings/.test((await r.json()).error), 'someone with no saved sign-in is told what to do');
console.log('audiobooks ok');
} catch (e) { console.error(e); process.exitCode = 1; }
hub.close(); player.close();
});
});