131 lines
4.7 KiB
JavaScript
131 lines
4.7 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { readFile } from 'node:fs/promises'
|
|
import { Readable } from 'node:stream'
|
|
import path from 'node:path'
|
|
import { writeFile } from '../atomic-write.js'
|
|
import { requireAdminAuth } from '../auth.js'
|
|
import { loadHistory } from './nuggets-host-upload.js'
|
|
import {
|
|
DATA_DIR,
|
|
SEAFILE_URL,
|
|
SEAFILE_TOKEN,
|
|
SEAFILE_NUGGETS_REPO_ID,
|
|
SEAFILE_EPISODE_UPLOAD_DIR,
|
|
} from '../config.js'
|
|
|
|
const WORKFLOW_FILE = path.join(DATA_DIR, 'nuggets-workflow.json')
|
|
|
|
export const WORKFLOW_STEPS = ['listen', 'coldOpen', 'recordColdOpen', 'edit', 'spotify']
|
|
|
|
async function loadWorkflow() {
|
|
try {
|
|
return JSON.parse(await readFile(WORKFLOW_FILE, 'utf8'))
|
|
} catch {
|
|
return { items: {} }
|
|
}
|
|
}
|
|
|
|
function entryId(entry) {
|
|
return createHash('sha1').update(`${entry.uploadedAt}|${entry.filename}`).digest('hex').slice(0, 16)
|
|
}
|
|
|
|
function emptyState() {
|
|
return {
|
|
steps: Object.fromEntries(WORKFLOW_STEPS.map(s => [s, null])), // step → ISO completion time
|
|
coldOpenText: '',
|
|
notes: '',
|
|
spotifyUrl: '',
|
|
}
|
|
}
|
|
|
|
function seafileConfigured() {
|
|
return Boolean(SEAFILE_URL && SEAFILE_TOKEN && SEAFILE_NUGGETS_REPO_ID)
|
|
}
|
|
|
|
export function register(app) {
|
|
app.get('/api/admin/nuggets-workflow', requireAdminAuth, async (_req, res) => {
|
|
const [history, wf] = await Promise.all([loadHistory(), loadWorkflow()])
|
|
const items = history.entries.map(entry => {
|
|
const id = entryId(entry)
|
|
return { id, entry, state: { ...emptyState(), ...(wf.items[id] ?? {}) } }
|
|
})
|
|
res.json({ items })
|
|
})
|
|
|
|
app.put('/api/admin/nuggets-workflow/:id', requireAdminAuth, async (req, res) => {
|
|
const { id } = req.params
|
|
const history = await loadHistory()
|
|
if (!history.entries.some(e => entryId(e) === id)) {
|
|
res.status(404).json({ message: 'Upload not found.' })
|
|
return
|
|
}
|
|
const wf = await loadWorkflow()
|
|
const prev = { ...emptyState(), ...(wf.items[id] ?? {}) }
|
|
const body = req.body ?? {}
|
|
const next = { ...prev, steps: { ...prev.steps } }
|
|
|
|
if (body.steps && typeof body.steps === 'object') {
|
|
for (const s of WORKFLOW_STEPS) {
|
|
if (typeof body.steps[s] === 'boolean') {
|
|
// Keep the original completion time if the step was already done
|
|
next.steps[s] = body.steps[s] ? (prev.steps[s] ?? new Date().toISOString()) : null
|
|
}
|
|
}
|
|
}
|
|
if (typeof body.coldOpenText === 'string') next.coldOpenText = body.coldOpenText.slice(0, 5000)
|
|
if (typeof body.notes === 'string') next.notes = body.notes.slice(0, 5000)
|
|
if (typeof body.spotifyUrl === 'string') next.spotifyUrl = body.spotifyUrl.trim().slice(0, 500)
|
|
|
|
wf.items[id] = next
|
|
await writeFile(WORKFLOW_FILE, JSON.stringify(wf, null, 2), 'utf8')
|
|
res.json({ ok: true, state: next })
|
|
})
|
|
|
|
// Stream the uploaded audio from Seafile so it can be auditioned in the admin.
|
|
app.get('/api/admin/nuggets-workflow/:id/audio', requireAdminAuth, async (req, res) => {
|
|
if (!seafileConfigured()) {
|
|
res.status(503).json({ message: 'Seafile is not configured.' })
|
|
return
|
|
}
|
|
const history = await loadHistory()
|
|
const entry = history.entries.find(e => entryId(e) === req.params.id)
|
|
if (!entry) {
|
|
res.status(404).json({ message: 'Upload not found.' })
|
|
return
|
|
}
|
|
try {
|
|
const filePath = `${SEAFILE_EPISODE_UPLOAD_DIR.replace(/\/$/, '')}/${entry.filename}`
|
|
const linkRes = await fetch(
|
|
`${SEAFILE_URL}/api2/repos/${SEAFILE_NUGGETS_REPO_ID}/file/?p=${encodeURIComponent(filePath)}&reuse=1`,
|
|
{ headers: { Authorization: `Token ${SEAFILE_TOKEN}` }, signal: AbortSignal.timeout(10_000) },
|
|
)
|
|
if (!linkRes.ok) {
|
|
res.status(404).json({ message: 'Audio file not found in Seafile.' })
|
|
return
|
|
}
|
|
const downloadUrl = (await linkRes.text()).replace(/^"|"$/g, '')
|
|
|
|
const headers = {}
|
|
if (req.headers.range) headers.Range = req.headers.range
|
|
const upstream = await fetch(downloadUrl, { headers })
|
|
if (!upstream.ok && upstream.status !== 206) {
|
|
res.status(502).json({ message: 'Could not fetch audio from Seafile.' })
|
|
return
|
|
}
|
|
res.status(upstream.status)
|
|
for (const h of ['content-type', 'content-length', 'content-range', 'accept-ranges']) {
|
|
const v = upstream.headers.get(h)
|
|
if (v) res.setHeader(h, v)
|
|
}
|
|
if (!upstream.headers.get('content-type')) res.setHeader('content-type', 'audio/mpeg')
|
|
const stream = Readable.fromWeb(upstream.body)
|
|
req.on('close', () => stream.destroy())
|
|
stream.on('error', () => res.end())
|
|
stream.pipe(res)
|
|
} catch (err) {
|
|
console.error('[nuggets-workflow] audio error:', err.message)
|
|
if (!res.headersSent) res.status(500).json({ message: 'Audio stream failed.' })
|
|
}
|
|
})
|
|
}
|