MB-X Bilibili Pipeline
6 days ago be13e048ef833ce9407d9794e7dd7077c3af5f79
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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 === "已经到底了",
  };
}