import { decodeEntities, stripTags } from "./util/text"; export interface PacktClip { number: number; title: string; video: string; } export interface PacktSection { number: number; title: string; clips: PacktClip[] } export interface PacktIndex { title: string; sections: PacktSection[]; warnings: string[] } /** * Parses the Packt player's index.html structurally: * - the section overview: `
]*>\s*Video\s+\d+\.\d+\s*:\s*([\s\S]*?)<\/p>/i.exec(block);
if (items.length === 0 && firstP) items.push(stripTags(firstP[1]!));
overviewTitles.set(n, items);
});
// chapters: clips with their files
const chapterClips = new Map]*>\s*(?:Video\s+)?(\d+)\.(\d+)\s*[:.]?\s*([\s\S]*?)<\/h1>/gi;
const hs = [...block.matchAll(clipRe)];
const clips: PacktClip[] = [];
hs.forEach((h, j) => {
const part = block.slice(h.index!, hs[j + 1]?.index ?? block.length);
const vid = /data-video="([^"]+)"/i.exec(part)?.[1] ?? /href="([^"]+\.(?:mp4|webm|m4v|mov))"/i.exec(part)?.[1];
const num = Number(h[2]);
if (!vid) { warnings.push(`clip ${sec}.${num} has no video file in index.html`); return; }
clips.push({ number: num, title: stripTags(h[3]!), video: decodeEntities(vid) });
});
chapterClips.set(sec, clips);
});
const sections: PacktSection[] = [];
for (const n of [...sectionTitles.keys()].sort((a, b) => a - b)) {
let clips = chapterClips.get(n);
const titles = overviewTitles.get(n) ?? [];
if (!clips || clips.length === 0) {
warnings.push(`section ${n} has no chapter block; clips were taken from the overview list and mapped to videos/video${n}_M.mp4 in order`);
clips = titles.map((t, i) => ({ number: i + 1, title: t, video: `videos/video${n}_${i + 1}.mp4` }));
} else {
clips = clips.map((c, i) => ({ ...c, title: c.title || titles[i] || `Video ${n}.${c.number}` }));
if (titles.length && titles.length !== clips.length) warnings.push(`section ${n}: overview lists ${titles.length} clips but the chapter has ${clips.length}`);
}
sections.push({ number: n, title: sectionTitles.get(n)!, clips });
}
return { title, sections, warnings };
}