import { normalizeFractionRateDisplay } from "../../shared/rate-normalizer"; import { writeMarketListRequestSnapshot } from "./market-list-request-snapshot"; import { parseMarketListResponse } from "./market-list-row"; const BRIDGE_MARKER = "__SCES_MARKET_PAGE_BRIDGE_INSTALLED__"; const MARKET_SEARCH_REQUEST_PATH = "/gw/api/gsearch/search_for_author_square"; const SERIALIZED_MARKET_ROWS_ATTRIBUTE = "data-sces-market-rows"; type MarketRow = { attribute_datas?: Record; nick_name?: string; star_id?: string; }; declare global { interface Window { [BRIDGE_MARKER]?: boolean; } } installMarketPageBridge(); function installMarketPageBridge() { if (window[BRIDGE_MARKER]) { syncSerializedMarketRows(); return; } window[BRIDGE_MARKER] = true; installMarketRequestSnapshotBridge(); syncSerializedMarketRows(); const observer = new MutationObserver(() => { syncSerializedMarketRows(); }); observer.observe(document.documentElement, { childList: true, subtree: true }); window.setInterval(() => { syncSerializedMarketRows(); }, 1000); } function installMarketRequestSnapshotBridge() { installFetchSnapshotBridge(); installXmlHttpRequestSnapshotBridge(); } function syncSerializedMarketRows() { if (typeof document === "undefined") { return; } const nextSerializedRows = JSON.stringify(readSerializedMarketRows()); if ( document.documentElement.getAttribute(SERIALIZED_MARKET_ROWS_ATTRIBUTE) !== nextSerializedRows ) { document.documentElement.setAttribute( SERIALIZED_MARKET_ROWS_ATTRIBUTE, nextSerializedRows ); } } function installFetchSnapshotBridge() { if (typeof window.fetch !== "function") { return; } const originalFetch = window.fetch.bind(window); window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { const requestSnapshot = readFetchSnapshot(input, init); const response = await originalFetch(input, init); if (requestSnapshot) { const clonedResponse = response.clone(); void captureMarketSnapshotFromResponse(requestSnapshot, () => clonedResponse.json() ); } return response; }; } function installXmlHttpRequestSnapshotBridge() { const OriginalXmlHttpRequest = window.XMLHttpRequest; if (!OriginalXmlHttpRequest) { return; } const originalOpen = OriginalXmlHttpRequest.prototype.open; const originalSend = OriginalXmlHttpRequest.prototype.send; const originalSetRequestHeader = OriginalXmlHttpRequest.prototype.setRequestHeader; OriginalXmlHttpRequest.prototype.open = function ( method: string, url: string | URL, ...rest: unknown[] ) { ( this as XMLHttpRequest & { __scesMarketSnapshot?: { headers: Record; method: string; url: string; }; } ).__scesMarketSnapshot = { headers: {}, method, url: String(url) }; return originalOpen.call(this, method, url, ...(rest as [boolean?, string?, string?])); }; OriginalXmlHttpRequest.prototype.setRequestHeader = function ( name: string, value: string ) { ( this as XMLHttpRequest & { __scesMarketSnapshot?: { headers: Record; }; } ).__scesMarketSnapshot?.headers && ((this as XMLHttpRequest & { __scesMarketSnapshot?: { headers: Record; }; }).__scesMarketSnapshot!.headers[name] = value); return originalSetRequestHeader.call(this, name, value); }; OriginalXmlHttpRequest.prototype.send = function (body?: Document | XMLHttpRequestBodyInit | null) { const snapshotState = ( this as XMLHttpRequest & { __scesMarketSnapshot?: { body?: string; headers: Record; method: string; url: string; }; } ).__scesMarketSnapshot; if (snapshotState) { snapshotState.body = typeof body === "string" ? body : undefined; this.addEventListener("load", () => { if (this.status < 200 || this.status >= 300 || typeof this.responseText !== "string") { return; } void captureMarketSnapshotFromResponse(snapshotState, async () => JSON.parse(this.responseText) ); }); } return originalSend.call(this, body); }; } async function captureMarketSnapshotFromResponse( snapshot: { body?: string; headers?: Record; method: string; url: string; }, readPayload: () => Promise ) { if (!isMarketSearchRequest(snapshot.url)) { return; } try { const payload = await readPayload(); if (!parseMarketListResponse(payload)) { return; } writeMarketListRequestSnapshot(document, { body: snapshot.body, headers: snapshot.headers, method: snapshot.method, url: snapshot.url }); } catch {} } function readSerializedMarketRows() { const marketList = readMarketList(); return marketList .map((row) => { const attributeDatas = isRecord(row.attribute_datas) ? row.attribute_datas : {}; const singleVideoAfterSearchRate = readNormalizedFractionRate( attributeDatas.avg_search_after_view_rate_30d ); return { authorId: readString(row.star_id) ?? readString(attributeDatas.id) ?? "", authorName: readString(attributeDatas.nickname) ?? readString(row.nick_name) ?? "", coreUserId: readString(attributeDatas.core_user_id) ?? undefined, singleVideoAfterSearchRate }; }) .filter((row) => Boolean(row.authorId || row.authorName)); } function readFetchSnapshot( input: RequestInfo | URL, init?: RequestInit ): { body?: string; headers?: Record; method: string; url: string; } | null { const request = input instanceof Request ? input : null; const method = init?.method ?? request?.method ?? "GET"; const url = request?.url ?? String(input); const body = typeof init?.body === "string" ? init.body : typeof request?.bodyUsed === "boolean" && request.bodyUsed ? undefined : undefined; const headers = serializeHeaders(init?.headers ?? request?.headers); return { body, headers, method, url }; } function serializeHeaders( headers: HeadersInit | undefined ): Record | undefined { if (!headers) { return undefined; } if (headers instanceof Headers) { return Object.fromEntries(headers.entries()); } if (Array.isArray(headers)) { return Object.fromEntries(headers); } return Object.fromEntries( Object.entries(headers).map(([key, value]) => [key, String(value)]) ); } function readMarketList(): MarketRow[] { if (typeof document === "undefined") { return []; } const marketRoot = document.querySelector(".base-author-list") as | (HTMLElement & { __vue__?: { _setupState?: Record; }; }) | null; const setupState = marketRoot?.__vue__?._setupState; if (!setupState) { return []; } for (const value of Object.values(setupState)) { const candidate = unwrapVueRef(value); if (Array.isArray(candidate) && looksLikeMarketList(candidate)) { return candidate as MarketRow[]; } if (!isRecord(candidate) || !Array.isArray(candidate.marketList)) { continue; } if (looksLikeMarketList(candidate.marketList)) { return candidate.marketList as MarketRow[]; } } return []; } function isMarketSearchRequest(url: string): boolean { return ( url === MARKET_SEARCH_REQUEST_PATH || url.startsWith(`${MARKET_SEARCH_REQUEST_PATH}?`) || url.includes(`${MARKET_SEARCH_REQUEST_PATH}?`) || url.endsWith(MARKET_SEARCH_REQUEST_PATH) ); } function looksLikeMarketList(value: unknown[]): boolean { const firstRow = value[0]; return isRecord(firstRow) && ("star_id" in firstRow || "attribute_datas" in firstRow); } function unwrapVueRef(value: unknown): unknown { if (isRecord(value) && "value" in value) { return value.value; } return value; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function readString(value: unknown): string | null { return typeof value === "string" ? value : null; } function readNormalizedFractionRate(value: unknown): string | undefined { return typeof value === "string" ? normalizeFractionRateDisplay(value) ?? undefined : undefined; }