Add market watch and match hub workflows
这个提交包含在:
112
server/tutorialMetrics.ts
普通文件
112
server/tutorialMetrics.ts
普通文件
@@ -0,0 +1,112 @@
|
||||
import { ENV } from "./_core/env";
|
||||
|
||||
export type TutorialMetrics = {
|
||||
viewCount?: number;
|
||||
commentCount?: number;
|
||||
thumbnailUrl?: string;
|
||||
fetchedAt: Date;
|
||||
};
|
||||
|
||||
export type TutorialMetricSource = {
|
||||
sourcePlatform?: string | null;
|
||||
platformVideoId?: string | null;
|
||||
metricsFetchedAt?: Date | string | null;
|
||||
viewCount?: number | null;
|
||||
commentCount?: number | null;
|
||||
};
|
||||
|
||||
const METRIC_CACHE_MS = 12 * 60 * 60 * 1000;
|
||||
|
||||
function parseCount(value: unknown) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function shouldRefreshTutorialMetrics(source: TutorialMetricSource) {
|
||||
if (!source.sourcePlatform || !source.platformVideoId) return false;
|
||||
if (source.sourcePlatform !== "bilibili" && source.sourcePlatform !== "youtube") return false;
|
||||
|
||||
const fetchedAt =
|
||||
source.metricsFetchedAt instanceof Date
|
||||
? source.metricsFetchedAt
|
||||
: source.metricsFetchedAt
|
||||
? new Date(source.metricsFetchedAt)
|
||||
: null;
|
||||
|
||||
if (!fetchedAt || Number.isNaN(fetchedAt.getTime())) return true;
|
||||
if (source.viewCount == null && source.commentCount == null) return true;
|
||||
return (Date.now() - fetchedAt.getTime()) > METRIC_CACHE_MS;
|
||||
}
|
||||
|
||||
export async function fetchTutorialMetrics(sourcePlatform: string, platformVideoId: string): Promise<TutorialMetrics | null> {
|
||||
if (sourcePlatform === "bilibili") {
|
||||
return fetchBilibiliMetrics(platformVideoId);
|
||||
}
|
||||
if (sourcePlatform === "youtube") {
|
||||
return fetchYouTubeMetrics(platformVideoId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchBilibiliMetrics(bvid: string): Promise<TutorialMetrics | null> {
|
||||
const response = await fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${encodeURIComponent(bvid)}`, {
|
||||
headers: {
|
||||
"accept": "application/json",
|
||||
"user-agent": "tennis-training-hub/1.0",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Bilibili metrics request failed with ${response.status}`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
const stat = payload?.data?.stat;
|
||||
if (!payload?.data || !stat) return null;
|
||||
|
||||
return {
|
||||
viewCount: parseCount(stat.view),
|
||||
commentCount: parseCount(stat.reply),
|
||||
thumbnailUrl: typeof payload.data.pic === "string" ? payload.data.pic.replace(/^http:\/\//, "https://") : undefined,
|
||||
fetchedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchYouTubeMetrics(videoId: string): Promise<TutorialMetrics | null> {
|
||||
if (!ENV.youtubeApiKey) return null;
|
||||
|
||||
const url = new URL("https://www.googleapis.com/youtube/v3/videos");
|
||||
url.searchParams.set("part", "statistics,snippet");
|
||||
url.searchParams.set("id", videoId);
|
||||
url.searchParams.set("key", ENV.youtubeApiKey);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
"accept": "application/json",
|
||||
"user-agent": "tennis-training-hub/1.0",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`YouTube metrics request failed with ${response.status}`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
const item = Array.isArray(payload?.items) ? payload.items[0] : null;
|
||||
if (!item) return null;
|
||||
|
||||
return {
|
||||
viewCount: parseCount(item.statistics?.viewCount),
|
||||
commentCount: parseCount(item.statistics?.commentCount),
|
||||
thumbnailUrl:
|
||||
item.snippet?.thumbnails?.maxres?.url ||
|
||||
item.snippet?.thumbnails?.high?.url ||
|
||||
item.snippet?.thumbnails?.medium?.url ||
|
||||
item.snippet?.thumbnails?.default?.url,
|
||||
fetchedAt: new Date(),
|
||||
};
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户