function targetProof(value) {
|
const parsed = new URL(String(value));
|
const parts = parsed.pathname.split("/").filter(Boolean);
|
if (parsed.protocol !== "https:" || parsed.hostname !== "space.bilibili.com" || parsed.search || parsed.hash || parts.length !== 2 || !/^[1-9][0-9]{0,19}$/u.test(parts[0]) || parts[1] !== "dynamic") throw new Error("E_PAGE_URL");
|
return {uid: parts[0], dynamic_url: `https://space.bilibili.com/${parts[0]}/dynamic`, profile_url: `https://space.bilibili.com/${parts[0]}`};
|
}
|
|
function text(value) {
|
return String(value ?? "").replace(/\s+/gu, " ").trim();
|
}
|
|
function canonicalUrl(value, target) {
|
const proof = targetProof(target);
|
const parsed = new URL(String(value), proof.dynamic_url);
|
if (parsed.protocol !== "https:" || parsed.hostname !== "space.bilibili.com") throw new Error("E_PAGE_URL");
|
parsed.hash = "";
|
return parsed.toString();
|
}
|
|
export function canonicalizeSnapshot(raw, target) {
|
const proof = targetProof(target);
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("E_PAGE_SHAPE");
|
const cards = Array.isArray(raw.cards) ? raw.cards : [];
|
const normalized = cards.map((card, position) => {
|
if (!card || typeof card !== "object" || Array.isArray(card)) throw new Error("E_CARD_SHAPE");
|
const dynamicId = text(card.dynamic_id);
|
const publishedAt = text(card.published_at);
|
const body = text(card.text);
|
const url = canonicalUrl(card.url, target);
|
if (!/^\d{1,32}$/u.test(dynamicId) || !publishedAt || !body) throw new Error("E_CARD_IDENTITY");
|
return {position, dynamic_id: dynamicId, published_at: publishedAt, text: body, url};
|
});
|
const seen = new Set();
|
for (const card of normalized) {
|
if (seen.has(card.dynamic_id)) throw new Error("E_CARD_DUPLICATE");
|
seen.add(card.dynamic_id);
|
}
|
const marker = text(raw.terminal_marker_text);
|
if (text(raw.creator?.uid) !== proof.uid || canonicalUrl(raw.creator?.profile_url, target).replace(/\/$/u, "") !== proof.profile_url) throw new Error("E_CREATOR_IDENTITY");
|
return {
|
schema_version: 1,
|
final_url: canonicalUrl(raw.final_url, target),
|
page_title: text(raw.page_title),
|
ready_state: text(raw.ready_state),
|
visibility_state: text(raw.visibility_state),
|
creator: {uid: proof.uid, name: text(raw.creator?.name), profile_url: proof.profile_url},
|
cards: normalized,
|
unparsed_nodes: Number(raw.unparsed_nodes ?? 0),
|
terminal_marker_text: marker,
|
coverage_complete: marker === "已经到底了" && Number(raw.unparsed_nodes ?? 0) === 0,
|
};
|
}
|
|
export function collectVisiblePage(target) {
|
const proof = targetProof(target);
|
const normalizeText = (value) => String(value ?? "").replace(/\s+/gu, " ").trim();
|
const normalizeUrl = (value) => {
|
const parsed = new URL(String(value), proof.dynamic_url);
|
if (parsed.protocol !== "https:" || parsed.hostname !== "space.bilibili.com") throw new Error("E_PAGE_URL");
|
parsed.hash = "";
|
return parsed.toString();
|
};
|
if (normalizeUrl(location.href).replace(/\/$/u, "") !== proof.dynamic_url) throw new Error("E_PAGE_IDENTITY");
|
const cards = [...document.querySelectorAll("[data-dynamic-id]")].map((node) => ({
|
dynamic_id: normalizeText(node.getAttribute("data-dynamic-id")),
|
published_at: normalizeText(node.getAttribute("data-published-at") || node.querySelector("time")?.getAttribute("datetime")),
|
text: normalizeText(node.querySelector("[data-dynamic-text]")?.textContent || node.textContent),
|
url: normalizeUrl(node.querySelector("a[href]")?.href || location.href),
|
}));
|
const terminal = [...document.querySelectorAll("body *")].map((node) => String(node.textContent || "").trim()).find((value) => value === "已经到底了") || "";
|
const seen = new Set();
|
for (const card of cards) {
|
if (!/^\d{1,32}$/u.test(card.dynamic_id) || !card.published_at || !card.text || seen.has(card.dynamic_id)) throw new Error("E_CARD_IDENTITY");
|
seen.add(card.dynamic_id);
|
}
|
return {
|
schema_version: 1,
|
final_url: proof.dynamic_url,
|
page_title: normalizeText(document.title),
|
ready_state: normalizeText(document.readyState),
|
visibility_state: normalizeText(document.visibilityState),
|
creator: {uid: proof.uid, name: normalizeText(document.querySelector("h1")?.textContent || ""), profile_url: proof.profile_url},
|
cards: cards.map((card, position) => ({position, ...card})),
|
unparsed_nodes: 0,
|
terminal_marker_text: terminal,
|
coverage_complete: terminal === "已经到底了",
|
};
|
}
|