Podcast workflow: assign lanes by hand, match by season, unique keys for unseasoned episodes; drop old checklist data file; v2.16.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ac1b06f945
commit
900a3efee3
6 files changed
+69
-698
No files matched your search
@@ -14,9 +14,9 @@ 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 ?? {} }
|
||||
return { planned: Array.isArray(data.planned) ? data.planned : [], items: data.items ?? {}, laneOverrides: data.laneOverrides ?? {} }
|
||||
} catch {
|
||||
return { planned: [], items: {} }
|
||||
return { planned: [], items: {}, laneOverrides: {} }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ function laneFor(season) {
|
||||
return 'other'
|
||||
}
|
||||
|
||||
const normTitle = t => String(t ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()
|
||||
const plannedKey = p => p.key ?? scriptKey(p.season, p.episodeNumber)
|
||||
|
||||
function toIsoDate(pubDate) {
|
||||
const d = new Date(pubDate)
|
||||
return Number.isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10)
|
||||
@@ -65,17 +68,25 @@ export function register(app) {
|
||||
})
|
||||
}
|
||||
for (const p of wf.planned) {
|
||||
const key = scriptKey(p.season, p.episodeNumber)
|
||||
const key = plannedKey(p)
|
||||
const prev = byKey.get(key)
|
||||
upsert(key, { season: p.season, episodeNumber: p.episodeNumber, title: prev?.title || p.title || '', planned: true })
|
||||
upsert(key, { season: p.season, episodeNumber: p.episodeNumber, title: prev?.title || p.title || '', planned: true, plannedLane: p.lane })
|
||||
}
|
||||
for (const item of feed) {
|
||||
const hasNum = /^\d+$/.test(item.episode)
|
||||
const key = hasNum ? scriptKey(item.season, item.episode) : `t:${item.title}`
|
||||
let key
|
||||
if (item.season && hasNum) {
|
||||
key = scriptKey(item.season, item.episode)
|
||||
} else {
|
||||
// No season in the feed: pair with a script/planned episode of the same title, else keep it on its own.
|
||||
const t = normTitle(item.title)
|
||||
const match = [...byKey.values()].find(e => !e.released && normTitle(e.title) === t)
|
||||
key = match ? match.key : `t:${t}`
|
||||
}
|
||||
const prev = byKey.get(key)
|
||||
upsert(key, {
|
||||
season: item.season,
|
||||
episodeNumber: hasNum ? item.episode : '',
|
||||
season: item.season || prev?.season || '',
|
||||
episodeNumber: hasNum ? item.episode : (prev?.episodeNumber ?? ''),
|
||||
title: item.title || prev?.title || '',
|
||||
released: true,
|
||||
releaseDate: toIsoDate(item.pubDate),
|
||||
@@ -85,7 +96,8 @@ export function register(app) {
|
||||
|
||||
const items = [...byKey.values()].map(e => ({
|
||||
...e,
|
||||
lane: laneFor(e.season),
|
||||
lane: wf.laneOverrides[e.key] ?? e.plannedLane ?? laneFor(e.season),
|
||||
laneOverride: wf.laneOverrides[e.key] ?? '',
|
||||
released: Boolean(e.released),
|
||||
state: { ...emptyState(), ...(wf.items[e.key] ?? {}) },
|
||||
}))
|
||||
@@ -101,10 +113,12 @@ export function register(app) {
|
||||
res.status(400).json({ message: 'Episode number must be a whole number.' })
|
||||
return
|
||||
}
|
||||
const lane = ['A', 'B'].includes(req.body?.lane) ? req.body.lane : 'other'
|
||||
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 })
|
||||
// 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)
|
||||
}
|
||||
res.json({ ok: true, key })
|
||||
@@ -112,8 +126,23 @@ export function register(app) {
|
||||
|
||||
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)
|
||||
wf.planned = wf.planned.filter(p => plannedKey(p) !== req.params.key)
|
||||
delete wf.items[req.params.key]
|
||||
delete wf.laneOverrides[req.params.key]
|
||||
await save(wf)
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
// Assign an episode to a lane by hand ('auto' clears the override and goes back to season matching).
|
||||
app.put('/api/admin/podcast-workflow/:key/lane', requireAdminAuth, async (req, res) => {
|
||||
const lane = req.body?.lane
|
||||
if (!['A', 'B', 'other', 'auto'].includes(lane)) {
|
||||
res.status(400).json({ message: 'Lane must be A, B, other or auto.' })
|
||||
return
|
||||
}
|
||||
const wf = await load()
|
||||
if (lane === 'auto') delete wf.laneOverrides[req.params.key]
|
||||
else wf.laneOverrides[req.params.key] = lane
|
||||
await save(wf)
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user