← Public packages
@kentcdodds/audible
Personal Audible library app — browser re-auth, library, wishlist, NAS archive stub.
src/chapters.ts
72 lines · 2.2 KB · TypeScriptfunction asNumber(value: unknown): number | undefined {
if (typeof value === 'number' && Number.isFinite(value)) return value
if (typeof value === 'string' && value.trim()) {
const n = Number(value)
if (Number.isFinite(n)) return n
}
return undefined
}
export type ArchiveChapter = {
title: string
startMs?: number
lengthMs?: number
}
/**
* Flatten Audible chapter_info (tree or flat) into home MCP chapter objects.
* Uses start_offset_ms / length_ms from the license metadata.
*/
export function mapChapters(chapterInfo: unknown): ArchiveChapter[] | undefined {
if (!chapterInfo) return undefined
const root =
typeof chapterInfo === 'object' && !Array.isArray(chapterInfo)
? (chapterInfo as Record<string, unknown>)
: null
const list = Array.isArray(root?.chapters)
? root.chapters
: Array.isArray(chapterInfo)
? chapterInfo
: null
if (!list?.length) return undefined
const out: ArchiveChapter[] = []
const walk = (nodes: unknown[]) => {
for (const node of nodes) {
if (!node || typeof node !== 'object') continue
const chapter = node as Record<string, unknown>
const children = Array.isArray(chapter.chapters) ? chapter.chapters : null
if (children?.length) {
walk(children)
continue
}
const title =
typeof chapter.title === 'string' && chapter.title.trim()
? chapter.title.trim()
: 'Chapter'
const startMs =
asNumber(chapter.startMs) ?? asNumber(chapter.start_offset_ms)
const lengthMs = asNumber(chapter.lengthMs) ?? asNumber(chapter.length_ms)
out.push({ title, startMs, lengthMs })
}
}
walk(list)
return out.length ? out : undefined
}
export function titleFromLibraryPayload(payload: unknown): string | undefined {
if (!payload || typeof payload !== 'object') return undefined
const rec = payload as Record<string, unknown>
const nested =
rec.item && typeof rec.item === 'object'
? (rec.item as Record<string, unknown>)
: rec
if (typeof nested.title === 'string' && nested.title.trim()) {
return nested.title.trim()
}
if (typeof nested.raw_title === 'string' && nested.raw_title.trim()) {
return nested.raw_title.trim()
}
if (typeof rec.title === 'string' && rec.title.trim()) return rec.title.trim()
return undefined
}