75 lines
4.1 KiB
TypeScript
75 lines
4.1 KiB
TypeScript
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: `<h1>Section N: …</h1>` followed by a `<ul>` of clip titles, in order, usually WITHOUT "Video N.M:" prefixes;
|
|
* - the per-section chapter blocks (`class="chapter" id="content-N"`) with one `<li>` per clip: `<h1>N.M title</h1>` and `data-video="…"`.
|
|
* Chapter blocks are authoritative for the video file; the overview list fills in any title or whole section that is missing.
|
|
*/
|
|
export function parsePacktIndex(html: string): PacktIndex {
|
|
const warnings: string[] = [];
|
|
const intro = /<div[^>]*class="intro"[\s\S]*?<h1[^>]*>([\s\S]*?)<\/h1>/i.exec(html);
|
|
const titleTag = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
|
|
const title = stripTags(intro?.[1] ?? titleTag?.[1] ?? "") || "Imported course";
|
|
|
|
const chapterStart = html.search(/<div[^>]*class="chapter"/i);
|
|
const overview = chapterStart >= 0 ? html.slice(0, chapterStart) : html;
|
|
const chaptersHtml = chapterStart >= 0 ? html.slice(chapterStart) : "";
|
|
|
|
// overview: sections in document order
|
|
const secRe = /<h1[^>]*>\s*Section\s+(\d+)\s*[:.-]\s*([\s\S]*?)<\/h1>/gi;
|
|
const heads = [...overview.matchAll(secRe)];
|
|
if (heads.length === 0) throw new Error("not a Packt video package: no \"Section N: …\" headings found in index.html");
|
|
const overviewTitles = new Map<number, string[]>();
|
|
const sectionTitles = new Map<number, string>();
|
|
heads.forEach((h, i) => {
|
|
const n = Number(h[1]);
|
|
sectionTitles.set(n, stripTags(h[2]!));
|
|
const block = overview.slice(h.index! + h[0].length, heads[i + 1]?.index ?? overview.length);
|
|
const ul = /<ul[^>]*>([\s\S]*?)<\/ul>/i.exec(block);
|
|
const items = ul ? [...ul[1]!.matchAll(/<li[^>]*>([\s\S]*?)<\/li>/gi)].map((m) => stripTags(m[1]!)).filter(Boolean) : [];
|
|
const firstP = /<p[^>]*>\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<number, PacktClip[]>();
|
|
const chapRe = /<div[^>]*class="chapter"[^>]*id="content-(\d+)"[^>]*>/gi;
|
|
const chaps = [...chaptersHtml.matchAll(chapRe)];
|
|
chaps.forEach((c, i) => {
|
|
const sec = Number(c[1]);
|
|
const block = chaptersHtml.slice(c.index! + c[0].length, chaps[i + 1]?.index ?? chaptersHtml.length);
|
|
const clipRe = /<h1[^>]*>\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 };
|
|
}
|