Audiobooks: talk to OpenAudible directly with its own username and password (OPENAUDIBLE_URL)
build-container / build (push) Successful in 9m46s
build-container / build (push) Successful in 9m46s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9231315186
commit
aa2133ed9a
7 files changed
+86
-6
No files matched your search
@@ -0,0 +1,36 @@
|
||||
// Audiobooks straight from OpenAudible (OPENAUDIBLE_URL): its own username and password, byte ranges cut by the hub, positions kept by the hub per person.
|
||||
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-abd-'));
|
||||
process.env.DATA_DIR = dir; process.env.COOKIE_SECRET = 'abd-'.repeat(8);
|
||||
const seen = [], AUDIO = Buffer.from('0123456789');
|
||||
const oa = http.createServer((req, res) => {
|
||||
seen.push({ u: req.url, auth: req.headers.authorization, range: req.headers.range });
|
||||
if (req.headers.authorization !== 'Basic ' + Buffer.from('nate:oapw').toString('base64')) { res.writeHead(401); return res.end(); }
|
||||
if (req.url === '/api/books') { res.writeHead(200, { 'content-type': 'application/json' }); return res.end('[{"id":"b1","title":"One"}]'); }
|
||||
if (req.url === '/api/books/b1/files/b1.mp3') { res.writeHead(200, { 'content-type': 'audio/mpeg', 'content-length': AUDIO.length }); return res.end(AUDIO); } // ignores Range, like some servers
|
||||
res.writeHead(404); res.end();
|
||||
}).listen(0, async () => {
|
||||
process.env.OPENAUDIBLE_URL = 'http://127.0.0.1:' + oa.address().port;
|
||||
const users = require('../users'), abapi = require('../abapi'), me = { id: 'abd', name: 'N' };
|
||||
const put = c => { fs.mkdirSync(path.join(dir, 'users', me.id), { recursive: true }); fs.writeFileSync(path.join(dir, 'users', me.id, 'settings.json'), JSON.stringify({ ab_creds: c })); };
|
||||
put({ u: 'nate', p: 'oapw' });
|
||||
const hub = http.createServer((req, res) => users.runAs(me, () => abapi.handle(req, res))).listen(0, async () => {
|
||||
const base = 'http://127.0.0.1:' + hub.address().port + '/api/ab', get = (p, o) => fetch(base + p, o);
|
||||
try {
|
||||
let r = await get('/books'); assert.equal(r.status, 200); assert.equal((await r.json())[0].title, 'One');
|
||||
r = await get('/books/b1/files/b1.mp3', { headers: { range: 'bytes=2-5' } }); assert.equal(r.status, 206); assert.equal(await r.text(), '2345'); assert.equal(r.headers.get('content-range'), 'bytes 2-5/10');
|
||||
r = await get('/books/b1/files/b1.mp3', { headers: { range: 'bytes=7-' } }); assert.equal(await r.text(), '789');
|
||||
r = await get('/books/b1/files/b1.mp3'); assert.equal(r.status, 200); assert.equal((await r.text()).length, 10);
|
||||
const post = b => get('/progress', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(b) }).then(x => x.json());
|
||||
await post({ b1: { pos: 50, dur: 100, t: 10 } }); let p = await post({ b1: { pos: 5, dur: 100, t: 5 }, b2: { pos: 1, dur: 9, t: 1 } });
|
||||
assert.equal(p.b1.pos, 50, 'the newest position wins'); assert.equal(p.b2.pos, 1);
|
||||
assert.equal((await (await get('/progress')).json()).b1.pos, 50);
|
||||
put({ u: 'nate', p: 'wrong' });
|
||||
r = await get('/books'); assert.equal(r.status, 502); assert.ok(/did not accept the username and password/.test((await r.json()).error));
|
||||
put({});
|
||||
r = await get('/books'); assert.equal(r.status, 503); assert.ok(/OpenAudible username and password in Settings/.test((await r.json()).error));
|
||||
console.log('audiobooks direct ok');
|
||||
} catch (e) { console.error(e); process.exitCode = 1; }
|
||||
hub.close(); oa.close();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user