Episode scripts: file by season + episode so numbers can repeat per book; search labels for current series; v2.9.6
Publish Container / build (push) Successful in 16m51s
Publish Container / build (push) Successful in 16m51s
- Scripts keyed "<season>-<episode>" (legacy ones migrate on first start; unseasoned ones keep their plain key) - Season backfill handles episode numbers shared across seasons (title match fallback) - Fix: "Backfill seasons from RSS" was swallowed by the upload route - Global search labels script results with the title of the series playing now (both lanes), not just finished books Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ddedf0f81
commit
752611d121
7 files changed
+114
-41
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.9.5",
|
"version": "2.9.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+6
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { migrateScriptKeys } from './episode-script-keys.js'
|
||||||
import { mkdir, readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises'
|
import { mkdir, readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { randomUUID } from 'node:crypto'
|
import { randomUUID } from 'node:crypto'
|
||||||
@@ -804,7 +805,11 @@ export async function loadEpisodeScriptsFromDisk() {
|
|||||||
try {
|
try {
|
||||||
const raw = await readFile(EPISODE_SCRIPTS_FILE, 'utf8')
|
const raw = await readFile(EPISODE_SCRIPTS_FILE, 'utf8')
|
||||||
const parsed = JSON.parse(raw)
|
const parsed = JSON.parse(raw)
|
||||||
state.episodeScripts = (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {}
|
const loaded = (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {}
|
||||||
|
const { scripts, changed } = migrateScriptKeys(loaded)
|
||||||
|
state.episodeScripts = scripts
|
||||||
|
// Persist the move to season-episode keys once, so it isn't redone on every start
|
||||||
|
if (changed) queueEpisodeScriptsWrite()
|
||||||
} catch {
|
} catch {
|
||||||
state.episodeScripts = {}
|
state.episodeScripts = {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Episode scripts are filed by season + episode number ("2-1"), because each book of the Bible
|
||||||
|
// is its own podcast season and episode numbers can restart in every season. A script with no
|
||||||
|
// season keeps its plain episode number as its key.
|
||||||
|
|
||||||
|
export function scriptKey(season, episodeNumber) {
|
||||||
|
const ep = String(episodeNumber ?? '').trim()
|
||||||
|
const s = String(season ?? '').trim()
|
||||||
|
return /^\d+$/.test(s) ? `${s}-${ep}` : ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// The episode number a stored script belongs to (new entries record it; legacy ones are keyed by it).
|
||||||
|
export function scriptEpisodeNumber(key, entry) {
|
||||||
|
if (entry && typeof entry.episodeNumber === 'string' && entry.episodeNumber) return entry.episodeNumber
|
||||||
|
const parts = String(key).split('-')
|
||||||
|
return parts[parts.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-time move of legacy scripts ({ "7": { season: "1" } }) to season-episode keys ({ "1-7": ... }).
|
||||||
|
// Scripts without a season stay under their plain episode number. Never overwrites an existing key.
|
||||||
|
export function migrateScriptKeys(scripts) {
|
||||||
|
const next = {}
|
||||||
|
let changed = false
|
||||||
|
for (const [key, entry] of Object.entries(scripts ?? {})) {
|
||||||
|
if (!entry || typeof entry !== 'object') continue
|
||||||
|
const episodeNumber = scriptEpisodeNumber(key, entry)
|
||||||
|
const wanted = scriptKey(entry.season, episodeNumber)
|
||||||
|
const targetKey = wanted !== key && !(wanted in scripts) && !(wanted in next) ? wanted : key
|
||||||
|
const withEpisode = entry.episodeNumber === episodeNumber ? entry : { ...entry, episodeNumber }
|
||||||
|
if (targetKey !== key || withEpisode !== entry) changed = true
|
||||||
|
next[targetKey] = withEpisode
|
||||||
|
}
|
||||||
|
return { scripts: next, changed }
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { state } from '../state.js'
|
|||||||
import { queueEpisodeScriptsWrite } from '../data.js'
|
import { queueEpisodeScriptsWrite } from '../data.js'
|
||||||
import { MAX_EPISODE_SCRIPT_LENGTH } from '../config.js'
|
import { MAX_EPISODE_SCRIPT_LENGTH } from '../config.js'
|
||||||
import { fetchAllEpisodes } from './episodes.js'
|
import { fetchAllEpisodes } from './episodes.js'
|
||||||
|
import { scriptKey, scriptEpisodeNumber } from '../episode-script-keys.js'
|
||||||
|
|
||||||
function escapeRegExp(s) {
|
function escapeRegExp(s) {
|
||||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
@@ -30,14 +31,18 @@ function searchScripts(query) {
|
|||||||
const season = seasonQuery[1]
|
const season = seasonQuery[1]
|
||||||
return Object.entries(state.episodeScripts)
|
return Object.entries(state.episodeScripts)
|
||||||
.filter(([, entry]) => String(entry.season ?? '') === season)
|
.filter(([, entry]) => String(entry.season ?? '') === season)
|
||||||
.map(([episodeNumber, entry]) => ({
|
.map(([key, entry]) => {
|
||||||
episodeNumber,
|
const episodeNumber = scriptEpisodeNumber(key, entry)
|
||||||
title: entry.title ?? `Episode ${episodeNumber}`,
|
return {
|
||||||
filename: entry.filename ?? '',
|
key,
|
||||||
season: entry.season ?? null,
|
episodeNumber,
|
||||||
snippet: '',
|
title: entry.title ?? `Episode ${episodeNumber}`,
|
||||||
score: 1,
|
filename: entry.filename ?? '',
|
||||||
}))
|
season: entry.season ?? null,
|
||||||
|
snippet: '',
|
||||||
|
score: 1,
|
||||||
|
}
|
||||||
|
})
|
||||||
.sort((a, b) => Number(a.episodeNumber) - Number(b.episodeNumber))
|
.sort((a, b) => Number(a.episodeNumber) - Number(b.episodeNumber))
|
||||||
.slice(0, 8)
|
.slice(0, 8)
|
||||||
}
|
}
|
||||||
@@ -46,8 +51,9 @@ function searchScripts(query) {
|
|||||||
if (words.length === 0) return []
|
if (words.length === 0) return []
|
||||||
const results = []
|
const results = []
|
||||||
|
|
||||||
for (const [episodeNumber, entry] of Object.entries(state.episodeScripts)) {
|
for (const [key, entry] of Object.entries(state.episodeScripts)) {
|
||||||
if (!entry?.text) continue
|
if (!entry?.text) continue
|
||||||
|
const episodeNumber = scriptEpisodeNumber(key, entry)
|
||||||
const text = entry.text.toLowerCase()
|
const text = entry.text.toLowerCase()
|
||||||
|
|
||||||
const matchedWords = words.filter(w => containsWord(text, w))
|
const matchedWords = words.filter(w => containsWord(text, w))
|
||||||
@@ -67,6 +73,7 @@ function searchScripts(query) {
|
|||||||
if (end < entry.text.length) snippet = snippet + '…'
|
if (end < entry.text.length) snippet = snippet + '…'
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
|
key,
|
||||||
episodeNumber,
|
episodeNumber,
|
||||||
title: entry.title ?? `Episode ${episodeNumber}`,
|
title: entry.title ?? `Episode ${episodeNumber}`,
|
||||||
filename: entry.filename ?? '',
|
filename: entry.filename ?? '',
|
||||||
@@ -89,8 +96,9 @@ export function register(app) {
|
|||||||
|
|
||||||
// Admin: list all uploaded scripts
|
// Admin: list all uploaded scripts
|
||||||
app.get('/api/admin-episode-scripts', requireAdminAuth, (_req, res) => {
|
app.get('/api/admin-episode-scripts', requireAdminAuth, (_req, res) => {
|
||||||
const list = Object.entries(state.episodeScripts).map(([episodeNumber, entry]) => ({
|
const list = Object.entries(state.episodeScripts).map(([key, entry]) => ({
|
||||||
episodeNumber,
|
key,
|
||||||
|
episodeNumber: scriptEpisodeNumber(key, entry),
|
||||||
title: entry.title ?? `Episode ${episodeNumber}`,
|
title: entry.title ?? `Episode ${episodeNumber}`,
|
||||||
filename: entry.filename ?? '',
|
filename: entry.filename ?? '',
|
||||||
season: entry.season ?? null,
|
season: entry.season ?? null,
|
||||||
@@ -102,7 +110,9 @@ export function register(app) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Admin: upload a docx script for an episode
|
// Admin: upload a docx script for an episode
|
||||||
app.post('/api/admin-episode-scripts/:episodeNumber', requireAdminAuth, async (req, res) => {
|
app.post('/api/admin-episode-scripts/:episodeNumber', requireAdminAuth, async (req, res, next) => {
|
||||||
|
// "backfill-seasons" is its own route below; don't let this one swallow it
|
||||||
|
if (req.params.episodeNumber === 'backfill-seasons') { next(); return }
|
||||||
const episodeNumber = req.params.episodeNumber?.trim()
|
const episodeNumber = req.params.episodeNumber?.trim()
|
||||||
if (!episodeNumber || !/^\d+$/.test(episodeNumber)) {
|
if (!episodeNumber || !/^\d+$/.test(episodeNumber)) {
|
||||||
res.status(400).json({ message: 'Episode number must be a positive integer.' }); return
|
res.status(400).json({ message: 'Episode number must be a positive integer.' }); return
|
||||||
@@ -144,7 +154,13 @@ export function register(app) {
|
|||||||
text = text.slice(0, MAX_EPISODE_SCRIPT_LENGTH)
|
text = text.slice(0, MAX_EPISODE_SCRIPT_LENGTH)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.episodeScripts[episodeNumber] = {
|
const key = scriptKey(season, episodeNumber)
|
||||||
|
// A script with no season that this upload replaces would otherwise linger as a duplicate
|
||||||
|
if (season && state.episodeScripts[episodeNumber] && !state.episodeScripts[episodeNumber].season) {
|
||||||
|
delete state.episodeScripts[episodeNumber]
|
||||||
|
}
|
||||||
|
state.episodeScripts[key] = {
|
||||||
|
episodeNumber,
|
||||||
title,
|
title,
|
||||||
filename,
|
filename,
|
||||||
season,
|
season,
|
||||||
@@ -156,9 +172,10 @@ export function register(app) {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
key,
|
||||||
episodeNumber,
|
episodeNumber,
|
||||||
season,
|
season,
|
||||||
wordCount: state.episodeScripts[episodeNumber].wordCount,
|
wordCount: state.episodeScripts[key].wordCount,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -174,22 +191,36 @@ export function register(app) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const seasonByEpisodeNumber = new Map()
|
const normalize = value => String(value ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()
|
||||||
|
// Episode numbers can repeat across seasons, so keep every season an episode number appears in.
|
||||||
|
const feedByEpisodeNumber = new Map()
|
||||||
for (const ep of episodes) {
|
for (const ep of episodes) {
|
||||||
if (ep.episode && ep.season) seasonByEpisodeNumber.set(ep.episode, ep.season)
|
if (!ep.episode || !ep.season) continue
|
||||||
|
const list = feedByEpisodeNumber.get(ep.episode) ?? []
|
||||||
|
list.push({ season: String(ep.season), title: normalize(ep.title) })
|
||||||
|
feedByEpisodeNumber.set(ep.episode, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
let updated = 0
|
let updated = 0
|
||||||
const stillMissing = []
|
const stillMissing = []
|
||||||
for (const [episodeNumber, entry] of Object.entries(state.episodeScripts)) {
|
for (const [key, entry] of Object.entries({ ...state.episodeScripts })) {
|
||||||
if (entry.season) continue
|
if (entry.season) continue
|
||||||
const found = seasonByEpisodeNumber.get(episodeNumber)
|
const episodeNumber = scriptEpisodeNumber(key, entry)
|
||||||
if (found) {
|
const candidates = feedByEpisodeNumber.get(episodeNumber) ?? []
|
||||||
entry.season = found
|
let match = candidates.length === 1 ? candidates[0] : undefined
|
||||||
updated += 1
|
// Several seasons share this episode number: fall back to matching the title
|
||||||
} else {
|
if (!match && candidates.length > 1) {
|
||||||
stillMissing.push(episodeNumber)
|
const byTitle = candidates.filter(c => c.title && c.title === normalize(entry.title))
|
||||||
|
if (byTitle.length === 1) match = byTitle[0]
|
||||||
}
|
}
|
||||||
|
const newKey = match ? scriptKey(match.season, episodeNumber) : ''
|
||||||
|
if (!match || (newKey !== key && state.episodeScripts[newKey])) {
|
||||||
|
stillMissing.push(episodeNumber)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
delete state.episodeScripts[key]
|
||||||
|
state.episodeScripts[newKey] = { ...entry, season: match.season, episodeNumber }
|
||||||
|
updated += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updated > 0) queueEpisodeScriptsWrite()
|
if (updated > 0) queueEpisodeScriptsWrite()
|
||||||
@@ -198,12 +229,12 @@ export function register(app) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Admin: delete a script
|
// Admin: delete a script
|
||||||
app.delete('/api/admin-episode-scripts/:episodeNumber', requireAdminAuth, (req, res) => {
|
app.delete('/api/admin-episode-scripts/:key', requireAdminAuth, (req, res) => {
|
||||||
const episodeNumber = req.params.episodeNumber?.trim()
|
const key = req.params.key?.trim()
|
||||||
if (!state.episodeScripts[episodeNumber]) {
|
if (!key || !state.episodeScripts[key]) {
|
||||||
res.status(404).json({ message: 'Script not found.' }); return
|
res.status(404).json({ message: 'Script not found.' }); return
|
||||||
}
|
}
|
||||||
delete state.episodeScripts[episodeNumber]
|
delete state.episodeScripts[key]
|
||||||
queueEpisodeScriptsWrite()
|
queueEpisodeScriptsWrite()
|
||||||
res.json({ ok: true })
|
res.json({ ok: true })
|
||||||
})
|
})
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ export const state = {
|
|||||||
studyCertificates: [],
|
studyCertificates: [],
|
||||||
studyCertificatesWritePromise: Promise.resolve(),
|
studyCertificatesWritePromise: Promise.resolve(),
|
||||||
|
|
||||||
// episodeScripts: { [episodeNumber]: { text, title, filename, season, uploadedAt } }
|
// episodeScripts: { ["<season>-<episode>" or "<episode>" if no season]: { episodeNumber, text, title, filename, season, uploadedAt } }
|
||||||
episodeScripts: {},
|
episodeScripts: {},
|
||||||
episodeScriptsWritePromise: Promise.resolve(),
|
episodeScriptsWritePromise: Promise.resolve(),
|
||||||
|
|
||||||
|
|||||||
+9
-9
@@ -1670,7 +1670,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
|||||||
const [commentSearch, setCommentSearch] = useState('')
|
const [commentSearch, setCommentSearch] = useState('')
|
||||||
|
|
||||||
// Episode scripts state
|
// Episode scripts state
|
||||||
const [scriptsList, setScriptsList] = useState<Array<{ episodeNumber: string; title: string; filename: string; season: string | null; uploadedAt: string | null; wordCount: number }>>([])
|
const [scriptsList, setScriptsList] = useState<Array<{ key: string; episodeNumber: string; title: string; filename: string; season: string | null; uploadedAt: string | null; wordCount: number }>>([])
|
||||||
const [scriptsLoading, setScriptsLoading] = useState(false)
|
const [scriptsLoading, setScriptsLoading] = useState(false)
|
||||||
const [scriptUploadEpisode, setScriptUploadEpisode] = useState('')
|
const [scriptUploadEpisode, setScriptUploadEpisode] = useState('')
|
||||||
const [scriptUploadTitle, setScriptUploadTitle] = useState('')
|
const [scriptUploadTitle, setScriptUploadTitle] = useState('')
|
||||||
@@ -3148,7 +3148,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
|||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (!res.ok) { setScriptUploadMsg(data.message ?? 'Upload failed.'); return }
|
if (!res.ok) { setScriptUploadMsg(data.message ?? 'Upload failed.'); return }
|
||||||
setScriptUploadMsg(`✓ Episode ${scriptUploadEpisode} uploaded — ${data.wordCount?.toLocaleString()} words indexed.`)
|
setScriptUploadMsg(`✓ Episode ${scriptUploadEpisode}${scriptUploadSeason.trim() ? ` (Season ${scriptUploadSeason.trim()})` : ''} uploaded — ${data.wordCount?.toLocaleString()} words indexed.`)
|
||||||
setScriptUploadEpisode('')
|
setScriptUploadEpisode('')
|
||||||
setScriptUploadTitle('')
|
setScriptUploadTitle('')
|
||||||
setScriptUploadSeason('')
|
setScriptUploadSeason('')
|
||||||
@@ -3181,12 +3181,12 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteScript(episodeNumber: string) {
|
async function handleDeleteScript(key: string, label: string) {
|
||||||
if (!confirm(`Remove script for Episode ${episodeNumber}?`)) return
|
if (!confirm(`Remove script for ${label}?`)) return
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/admin-episode-scripts/${encodeURIComponent(episodeNumber)}`, { method: 'DELETE' })
|
const res = await fetch(`/api/admin-episode-scripts/${encodeURIComponent(key)}`, { method: 'DELETE' })
|
||||||
if (!res.ok) { alert('Failed to delete script.'); return }
|
if (!res.ok) { alert('Failed to delete script.'); return }
|
||||||
setScriptsList(prev => prev.filter(s => s.episodeNumber !== episodeNumber))
|
setScriptsList(prev => prev.filter(s => s.key !== key))
|
||||||
} catch { alert('Failed to delete script.') }
|
} catch { alert('Failed to delete script.') }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5684,7 +5684,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-field">
|
<div className="admin-field">
|
||||||
<label htmlFor="script-season">Season <span style={{ color: '#5a5440', fontWeight: 400 }}>(optional — matches the podcast season number)</span></label>
|
<label htmlFor="script-season">Season <span style={{ color: '#5a5440', fontWeight: 400 }}>(recommended — scripts are filed by season + episode, so episode numbers can repeat in each book)</span></label>
|
||||||
<input
|
<input
|
||||||
id="script-season"
|
id="script-season"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -5749,14 +5749,14 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
|||||||
{scriptsList
|
{scriptsList
|
||||||
.filter(script => scriptSeasonFilter === 'all' || script.season === scriptSeasonFilter)
|
.filter(script => scriptSeasonFilter === 'all' || script.season === scriptSeasonFilter)
|
||||||
.map(script => (
|
.map(script => (
|
||||||
<div key={script.episodeNumber} className="admin-script-row">
|
<div key={script.key} className="admin-script-row">
|
||||||
<div className="admin-script-row-info">
|
<div className="admin-script-row-info">
|
||||||
<span className="admin-script-ep">Ep. {script.episodeNumber}</span>
|
<span className="admin-script-ep">Ep. {script.episodeNumber}</span>
|
||||||
{script.season && <span className="admin-script-ep">Season {script.season}</span>}
|
{script.season && <span className="admin-script-ep">Season {script.season}</span>}
|
||||||
<span className="admin-script-title">{script.title}</span>
|
<span className="admin-script-title">{script.title}</span>
|
||||||
<span className="admin-script-meta">{script.wordCount.toLocaleString()} words · {script.filename}</span>
|
<span className="admin-script-meta">{script.wordCount.toLocaleString()} words · {script.filename}</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="btn-admin-remove" onClick={() => handleDeleteScript(script.episodeNumber)}>Remove</button>
|
<button type="button" className="btn-admin-remove" onClick={() => handleDeleteScript(script.key, `Episode ${script.episodeNumber}${script.season ? ` (Season ${script.season})` : ''}`)}>Remove</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface PublicQuestion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ScriptSearchResult {
|
interface ScriptSearchResult {
|
||||||
|
key?: string
|
||||||
episodeNumber: string
|
episodeNumber: string
|
||||||
title: string
|
title: string
|
||||||
filename: string
|
filename: string
|
||||||
@@ -121,8 +122,11 @@ export function useGlobalSearch(content: SiteContent) {
|
|||||||
for (const book of content.finishedBooks ?? []) {
|
for (const book of content.finishedBooks ?? []) {
|
||||||
if (book.season != null && book.title) map.set(String(book.season), book.title)
|
if (book.season != null && book.title) map.set(String(book.season), book.title)
|
||||||
}
|
}
|
||||||
|
// The series playing now (one or two at a time), so their scripts show a title too
|
||||||
|
if (content.seriesSeason != null && content.seriesTitle) map.set(String(content.seriesSeason), content.seriesTitle)
|
||||||
|
if (content.secondSeries?.season != null && content.secondSeries.title) map.set(String(content.secondSeries.season), content.secondSeries.title)
|
||||||
return map
|
return map
|
||||||
}, [content.finishedBooks])
|
}, [content.finishedBooks, content.seriesSeason, content.seriesTitle, content.secondSeries])
|
||||||
|
|
||||||
// Debounced server-side script search — runs whenever query changes
|
// Debounced server-side script search — runs whenever query changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -138,7 +142,7 @@ export function useGlobalSearch(content: SiteContent) {
|
|||||||
const results = (data.results ?? []).map(r => {
|
const results = (data.results ?? []).map(r => {
|
||||||
const seasonLabel = r.season ? (seasonTitleMap.get(r.season) ?? `Season ${r.season}`) : null
|
const seasonLabel = r.season ? (seasonTitleMap.get(r.season) ?? `Season ${r.season}`) : null
|
||||||
return {
|
return {
|
||||||
id: `script-${r.episodeNumber}`,
|
id: `script-${r.key ?? r.episodeNumber}`,
|
||||||
type: 'episode' as const,
|
type: 'episode' as const,
|
||||||
title: r.title,
|
title: r.title,
|
||||||
subtitle: [`Episode ${r.episodeNumber}`, seasonLabel, 'Script'].filter(Boolean).join(' · '),
|
subtitle: [`Episode ${r.episodeNumber}`, seasonLabel, 'Script'].filter(Boolean).join(' · '),
|
||||||
|
|||||||
Reference in new issue
Block a user