#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const https = require('https'); const CRED_PATH = path.join(__dirname, '..', '.credentials', 'audiobookshelf.env'); function loadCreds() { const raw = fs.readFileSync(CRED_PATH, 'utf8'); const env = {}; for (const line of raw.split('\n')) { const m = line.match(/^(\w+)=(.+)$/); if (m) env[m[1]] = m[2]; } return env; } function api(endpoint) { const { AUDIOBOOKSHELF_URL, AUDIOBOOKSHELF_TOKEN } = loadCreds(); const url = `${AUDIOBOOKSHELF_URL}${endpoint}`; return new Promise((resolve, reject) => { const req = https.get(url, { headers: { 'Authorization': `Bearer ${AUDIOBOOKSHELF_TOKEN}` } }, (res) => { let data = ''; res.on('data', c => data += c); res.on('end', () => { try { resolve(JSON.parse(data)); } catch { reject(new Error(`Invalid JSON from ${endpoint}`)); } }); }); req.on('error', reject); req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); }); }); } function formatDuration(secs) { const h = Math.floor(secs / 3600); const m = Math.floor((secs % 3600) / 60); if (h > 0) return `${h}h ${m}m`; return `${m}m`; } function formatProgress(progress) { return `${Math.round(progress * 100)}%`; } async function cmdCurrent() { const data = await api('/api/me/items-in-progress'); const items = data.libraryItems || []; if (items.length === 0) { console.log('Nothing in progress.'); return; } for (const item of items) { const meta = item.media?.metadata || {}; const title = meta.title || 'Unknown'; const author = meta.authorName || 'Unknown'; const narrator = meta.narratorName || '-'; const series = meta.seriesName || '-'; const duration = formatDuration(item.media?.duration || 0); // Get progress from user's mediaProgress const me = await api('/api/me'); const prog = (me.mediaProgress || []).find(p => p.libraryItemId === item.id); const progress = prog ? formatProgress(prog.progress) : '?'; const currentTime = prog ? formatDuration(prog.currentTime) : '?'; const remaining = prog ? formatDuration((item.media?.duration || 0) - (prog.currentTime || 0)) : '?'; console.log(`${title}\t${author}\t${series}\t${progress}\t${currentTime}/${duration}\tremaining:${remaining}\tnarrator:${narrator}`); } } async function cmdLibraries() { const data = await api('/api/libraries'); for (const lib of (data.libraries || [])) { console.log(`${lib.id}\t${lib.name}\t${lib.mediaType}`); } } async function cmdRecent(args) { const limit = args[0] || '5'; const me = await api('/api/me'); const progress = (me.mediaProgress || []) .filter(p => !p.isFinished) .sort((a, b) => (b.lastUpdate || 0) - (a.lastUpdate || 0)) .slice(0, parseInt(limit, 10)); if (progress.length === 0) { console.log('No recent listening activity.'); return; } for (const p of progress) { try { const item = await api(`/api/items/${p.libraryItemId}?expanded=1`); const meta = item.media?.metadata || {}; const title = meta.title || 'Unknown'; const author = meta.authorName || 'Unknown'; const series = meta.seriesName || '-'; const pct = formatProgress(p.progress); const lastUpdate = new Date(p.lastUpdate).toISOString().slice(0, 10); console.log(`${title}\t${author}\t${series}\t${pct}\tlast:${lastUpdate}`); } catch { // Item might have been removed } } } async function cmdFinished(args) { const limit = args[0] || '10'; const me = await api('/api/me'); const finished = (me.mediaProgress || []) .filter(p => p.isFinished) .sort((a, b) => (b.finishedAt || 0) - (a.finishedAt || 0)) .slice(0, parseInt(limit, 10)); if (finished.length === 0) { console.log('No finished books.'); return; } for (const p of finished) { try { const item = await api(`/api/items/${p.libraryItemId}?expanded=1`); const meta = item.media?.metadata || {}; const title = meta.title || 'Unknown'; const author = meta.authorName || 'Unknown'; const finishedDate = p.finishedAt ? new Date(p.finishedAt).toISOString().slice(0, 10) : '?'; console.log(`${title}\t${author}\tfinished:${finishedDate}`); } catch { // Item might have been removed } } } async function cmdStats() { const me = await api('/api/me'); const progress = me.mediaProgress || []; const inProgress = progress.filter(p => !p.isFinished && !p.hideFromContinueListening).length; const finished = progress.filter(p => p.isFinished).length; const totalListened = progress.reduce((sum, p) => sum + (p.currentTime || 0), 0); console.log(`in-progress:${inProgress}\tfinished:${finished}\ttotal-listened:${formatDuration(totalListened)}`); } // --- Main --- const [cmd, ...args] = process.argv.slice(2); const run = async () => { switch (cmd) { case 'current': await cmdCurrent(); break; case 'recent': await cmdRecent(args); break; case 'finished': await cmdFinished(args); break; case 'stats': await cmdStats(); break; case 'libraries': await cmdLibraries(); break; default: console.log(`Usage: audiobooks Commands: current Currently listening / in-progress books recent [limit] Recently active books (default: 5) finished [limit] Finished books (default: 10) stats Listening stats overview libraries List libraries`); } }; run().catch(err => { console.error(`Error: ${err.message}`); process.exit(1); });