Skip to content
← Public packages

@kentcdodds/audible

Personal Audible library app — browser re-auth, library, wishlist, NAS archive stub.

src/chapters.ts

72 lines · 2.2 KB · TypeScript
function 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
}