Files
Siteforge/server/routes/podcast-workflow.js
T

140 lines
4.8 KiB
JavaScript

import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { writeFile } from '../atomic-write.js'
import { requireAdminAuth } from '../auth.js'
import { state } from '../state.js'
import { DATA_DIR } from '../config.js'
import { scriptKey, scriptEpisodeNumber } from '../episode-script-keys.js'
import { fetchAllEpisodes } from './episodes.js'
const WORKFLOW_FILE = path.join(DATA_DIR, 'podcast-workflow.json')
const STEPS = ['readScript', 'record', 'edit', 'upload']
async function load() {
try {
const data = JSON.parse(await readFile(WORKFLOW_FILE, 'utf8'))
return { planned: Array.isArray(data.planned) ? data.planned : [], items: data.items ?? {} }
} catch {
return { planned: [], items: {} }
}
}
const save = data => writeFile(WORKFLOW_FILE, JSON.stringify(data, null, 2), 'utf8')
function emptyState() {
return {
steps: Object.fromEntries(STEPS.map(s => [s, null])),
notes: '',
}
}
function laneFor(season) {
const content = state.cachedSiteContent ?? {}
const s = String(season ?? '')
if (s && s === String(content.seriesSeason ?? '')) return 'A'
if (s && s === String(content.secondSeries?.season ?? '')) return 'B'
return 'other'
}
function toIsoDate(pubDate) {
const d = new Date(pubDate)
return Number.isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10)
}
export function register(app) {
app.get('/api/admin/podcast-workflow', requireAdminAuth, async (_req, res) => {
const wf = await load()
let feed = []
let feedOk = true
try {
feed = await fetchAllEpisodes()
} catch {
feedOk = false
}
const byKey = new Map()
const upsert = (key, patch) => byKey.set(key, { ...(byKey.get(key) ?? {}), key, ...patch })
for (const [key, entry] of Object.entries(state.episodeScripts ?? {})) {
upsert(key, {
season: entry.season ?? '',
episodeNumber: scriptEpisodeNumber(key, entry),
title: entry.title ?? '',
hasScript: true,
})
}
for (const p of wf.planned) {
const key = scriptKey(p.season, p.episodeNumber)
const prev = byKey.get(key)
upsert(key, { season: p.season, episodeNumber: p.episodeNumber, title: prev?.title || p.title || '', planned: true })
}
for (const item of feed) {
const hasNum = /^\d+$/.test(item.episode)
const key = hasNum ? scriptKey(item.season, item.episode) : `t:${item.title}`
const prev = byKey.get(key)
upsert(key, {
season: item.season,
episodeNumber: hasNum ? item.episode : '',
title: item.title || prev?.title || '',
released: true,
releaseDate: toIsoDate(item.pubDate),
spotifyUrl: item.link || '',
})
}
const items = [...byKey.values()].map(e => ({
...e,
lane: laneFor(e.season),
released: Boolean(e.released),
state: { ...emptyState(), ...(wf.items[e.key] ?? {}) },
}))
items.sort((a, b) => (Number(a.season) || 0) - (Number(b.season) || 0) || (Number(a.episodeNumber) || 0) - (Number(b.episodeNumber) || 0))
res.json({ items, feedOk })
})
app.post('/api/admin/podcast-workflow/planned', requireAdminAuth, async (req, res) => {
const season = /^\d+$/.test(String(req.body?.season ?? '')) ? String(req.body.season) : ''
const episodeNumber = String(req.body?.episodeNumber ?? '').trim()
const title = typeof req.body?.title === 'string' ? req.body.title.trim().slice(0, 160) : ''
if (!/^\d+$/.test(episodeNumber)) {
res.status(400).json({ message: 'Episode number must be a whole number.' })
return
}
const wf = await load()
const key = scriptKey(season, episodeNumber)
if (!wf.planned.some(p => scriptKey(p.season, p.episodeNumber) === key)) {
wf.planned.push({ season, episodeNumber, title })
await save(wf)
}
res.json({ ok: true, key })
})
app.delete('/api/admin/podcast-workflow/planned/:key', requireAdminAuth, async (req, res) => {
const wf = await load()
wf.planned = wf.planned.filter(p => scriptKey(p.season, p.episodeNumber) !== req.params.key)
delete wf.items[req.params.key]
await save(wf)
res.json({ ok: true })
})
app.put('/api/admin/podcast-workflow/:key', requireAdminAuth, async (req, res) => {
const { key } = req.params
const wf = await load()
const prev = { ...emptyState(), ...(wf.items[key] ?? {}) }
const next = { ...prev, steps: { ...prev.steps } }
const body = req.body ?? {}
if (body.steps && typeof body.steps === 'object') {
for (const s of STEPS) {
if (typeof body.steps[s] === 'boolean') {
next.steps[s] = body.steps[s] ? (prev.steps[s] ?? new Date().toISOString()) : null
}
}
}
if (typeof body.notes === 'string') next.notes = body.notes.slice(0, 5000)
wf.items[key] = next
await save(wf)
res.json({ ok: true, state: next })
})
}