159 lines
4.0 KiB
TypeScript
159 lines
4.0 KiB
TypeScript
import { normalizeRateDisplay } from "../../shared/rate-normalizer";
|
|
import type { MarketApiResult } from "./types";
|
|
|
|
interface FetchResponseLike {
|
|
json(): Promise<unknown>;
|
|
ok: boolean;
|
|
status?: number;
|
|
}
|
|
|
|
type FetchLike = (
|
|
input: string,
|
|
init?: RequestInit
|
|
) => Promise<FetchResponseLike>;
|
|
|
|
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<MarketApiResult> {
|
|
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<MarketApiResult> {
|
|
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<string, unknown> | 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<string, unknown> {
|
|
return typeof value === "object" && value !== null;
|
|
}
|