diff --git a/package-lock.json b/package-lock.json index d792a0d..3970b44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "siteforge", - "version": "2.16.1", + "version": "2.17.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "siteforge", - "version": "2.16.1", + "version": "2.17.0", "dependencies": { "@aws-sdk/client-s3": "^3.1135.0", "@dnd-kit/core": "^6.3.1", diff --git a/package.json b/package.json index 67b0628..1a7835e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.16.1", + "version": "2.17.0", "type": "module", "scripts": { "dev": "vite", diff --git a/server/routes/podcast-workflow.js b/server/routes/podcast-workflow.js index 36ed703..dc48bfd 100644 --- a/server/routes/podcast-workflow.js +++ b/server/routes/podcast-workflow.js @@ -4,7 +4,7 @@ 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 { scriptKey } from '../episode-script-keys.js' import { fetchAllEpisodes } from './episodes.js' const WORKFLOW_FILE = path.join(DATA_DIR, 'podcast-workflow.json') @@ -46,7 +46,7 @@ function laneFor(season, wf) { } const normTitle = t => String(t ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim() -const plannedKey = p => p.key ?? scriptKey(p.season, p.episodeNumber) +const plannedKey = p => scriptKey(p.season, p.episodeNumber) function toIsoDate(pubDate) { const d = new Date(pubDate) @@ -67,18 +67,10 @@ export function register(app) { 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 = plannedKey(p) const prev = byKey.get(key) - upsert(key, { season: p.season, episodeNumber: p.episodeNumber, title: prev?.title || p.title || '', planned: true, plannedLane: p.lane }) + 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) @@ -104,7 +96,7 @@ export function register(app) { const items = [...byKey.values()].map(e => ({ ...e, - lane: wf.laneOverrides[e.key] ?? e.plannedLane ?? laneFor(e.season, wf), + lane: wf.laneOverrides[e.key] ?? laneFor(e.season, wf), laneOverride: wf.laneOverrides[e.key] ?? '', released: Boolean(e.released), state: { ...emptyState(), ...(wf.items[e.key] ?? {}) }, @@ -113,22 +105,28 @@ export function register(app) { res.json({ items, feedOk, laneSeasons: { A: laneSeasonOf('A', wf), B: laneSeasonOf('B', wf) } }) }) + // Start the workflow for a new episode. Name, season and episode number are all required + // so the episode lands in the right lane and matches the feed once it is released. app.post('/api/admin/podcast-workflow/planned', requireAdminAuth, async (req, res) => { - const season = /^\d+$/.test(String(req.body?.season ?? '')) ? String(req.body.season) : '' + const season = String(req.body?.season ?? '').trim() 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.' }) + if (!title) { + res.status(400).json({ message: 'Episode name is required.' }) return } - const lane = ['A', 'B'].includes(req.body?.lane) ? req.body.lane : 'other' - const wf = await load() - // Without a season number the key must still be unique per lane. - const key = season ? scriptKey(season, episodeNumber) : `${lane}-p${episodeNumber}` - if (!wf.planned.some(p => plannedKey(p) === key)) { - wf.planned.push({ key, season, episodeNumber, title, lane }) - await save(wf) + if (!/^\d+$/.test(season) || !/^\d+$/.test(episodeNumber)) { + res.status(400).json({ message: 'Season and episode number must be whole numbers.' }) + return } + const wf = await load() + const key = scriptKey(season, episodeNumber) + if (wf.planned.some(p => plannedKey(p) === key)) { + res.status(409).json({ message: `Season ${season} episode ${episodeNumber} has already been started.` }) + return + } + wf.planned.push({ season, episodeNumber, title }) + await save(wf) res.json({ ok: true, key }) }) diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index 2e68ace..f4997e0 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -1820,7 +1820,6 @@ interface PodcastWorkflowItem { released: boolean releaseDate?: string spotifyUrl?: string - hasScript?: boolean planned?: boolean state: PodcastWorkflowState } @@ -1833,6 +1832,8 @@ function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string }; lane const [lane, setLane] = useState<'A' | 'B' | 'other'>('A') const [selectedKey, setSelectedKey] = useState('') const [showReleased, setShowReleased] = useState(false) + const [starting, setStarting] = useState(false) + const [newSeason, setNewSeason] = useState('') const [newNumber, setNewNumber] = useState('') const [newTitle, setNewTitle] = useState('') const [error, setError] = useState('') @@ -1888,11 +1889,11 @@ function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string }; lane const r = await fetch('/api/admin/podcast-workflow/planned', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ season: laneSeason, episodeNumber: newNumber, title: newTitle, lane }), + body: JSON.stringify({ season: newSeason, episodeNumber: newNumber, title: newTitle }), }) const d = await r.json() if (!r.ok) { setError(d.message ?? 'Could not add episode.'); return } - setNewNumber(''); setNewTitle('') + setNewNumber(''); setNewTitle(''); setStarting(false) await reload() setSelectedKey(d.key) } @@ -1929,7 +1930,6 @@ function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string }; lane {label(i)}
Nothing in progress.
} {upcoming.map(row)} - {lane !== 'other' && ( -