131 lines
3.3 KiB
TypeScript

import { normalizeFractionRateDisplay } from "../../shared/rate-normalizer";
const BRIDGE_MARKER = "__SCES_MARKET_PAGE_BRIDGE_INSTALLED__";
const SERIALIZED_MARKET_ROWS_ATTRIBUTE = "data-sces-market-rows";
type MarketRow = {
attribute_datas?: Record<string, unknown>;
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;
syncSerializedMarketRows();
const observer = new MutationObserver(() => {
syncSerializedMarketRows();
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
window.setInterval(() => {
syncSerializedMarketRows();
}, 1000);
}
function syncSerializedMarketRows() {
const nextSerializedRows = JSON.stringify(readSerializedMarketRows());
if (
document.documentElement.getAttribute(SERIALIZED_MARKET_ROWS_ATTRIBUTE) !==
nextSerializedRows
) {
document.documentElement.setAttribute(
SERIALIZED_MARKET_ROWS_ATTRIBUTE,
nextSerializedRows
);
}
}
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) ?? "",
singleVideoAfterSearchRate
};
})
.filter((row) => Boolean(row.authorId || row.authorName));
}
function readMarketList(): MarketRow[] {
const marketRoot = document.querySelector(".base-author-list") as
| (HTMLElement & {
__vue__?: {
_setupState?: Record<string, unknown>;
};
})
| 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 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<string, unknown> {
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;
}