import { normalizeRateDisplay } from "../../shared/rate-normalizer"; import type { MarketApiResult } from "./types"; interface FetchResponseLike { json(): Promise; ok: boolean; status?: number; } type FetchLike = ( input: string, init?: RequestInit ) => Promise; interface MarketApiClientOptions { baseUrl?: string; fetchImpl?: FetchLike; timeoutMs?: number; } export function createMarketApiClient(options: MarketApiClientOptions = {}) { const baseUrl = options.baseUrl ?? resolveBaseUrl(); const fetchImpl = options.fetchImpl ?? defaultFetch; const timeoutMs = options.timeoutMs ?? 8000; return { async loadAuthorAseInfo(authorId: string): Promise { const primaryResult = await loadAuthorMetricsFromUrl( buildAuthorCommerceSeedBaseInfoUrl(authorId, baseUrl) ); if (primaryResult.success || primaryResult.reason === "timeout") { return primaryResult; } return loadAuthorMetricsFromUrl(buildAuthorAseInfoUrl(authorId, baseUrl)); } }; async function loadAuthorMetricsFromUrl(url: string): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetchImpl(url, { credentials: "include", method: "GET", signal: controller.signal }); if (!response.ok) { return { success: false, reason: "request-failed" }; } return mapAuthorAseInfoResponse(await response.json()); } catch (error) { if (isAbortError(error) || controller.signal.aborted) { return { success: false, reason: "timeout" }; } return { success: false, reason: "request-failed" }; } finally { clearTimeout(timeoutId); } } } export function buildAuthorAseInfoUrl(authorId: string, baseUrl: string): string { const url = new URL("/gw/api/aggregator/get_author_ase_info", baseUrl); url.searchParams.set("author_id", authorId); url.searchParams.set("range", "30"); return url.toString(); } export function buildAuthorCommerceSeedBaseInfoUrl( authorId: string, baseUrl: string ): string { const url = new URL( "/gw/api/aggregator/get_author_commerce_seed_base_info", baseUrl ); url.searchParams.set("o_author_id", authorId); url.searchParams.set("range", "90"); return url.toString(); } export function mapAuthorAseInfoResponse(payload: unknown): MarketApiResult { const data = getPayloadData(payload); if (!data) { return { success: false, reason: "bad-response" }; } const singleVideoAfterSearchRate = readNormalizedRate( data.avg_search_after_view_rate ); const personalVideoAfterSearchRate = readNormalizedRate( data.personal_avg_search_after_view_rate ); if (!singleVideoAfterSearchRate && !personalVideoAfterSearchRate) { return { success: false, reason: "missing-rate" }; } return { success: true, rates: { ...(singleVideoAfterSearchRate ? { singleVideoAfterSearchRate } : {}), ...(personalVideoAfterSearchRate ? { personalVideoAfterSearchRate } : {}) } }; } function getPayloadData(payload: unknown): Record | null { if (!isRecord(payload)) { return null; } return isRecord(payload.data) ? payload.data : payload; } function readNormalizedRate(value: unknown): string | null { return typeof value === "string" ? normalizeRateDisplay(value) : null; } function resolveBaseUrl(): string { if (typeof location !== "undefined" && location.origin) { return location.origin; } return "https://xingtu.cn"; } async function defaultFetch(input: string, init?: RequestInit) { return fetch(input, init); } function isAbortError(error: unknown): boolean { return error instanceof Error && error.name === "AbortError"; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; }