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 { if (sourcePlatform === "bilibili") { return fetchBilibiliMetrics(platformVideoId); } if (sourcePlatform === "youtube") { return fetchYouTubeMetrics(platformVideoId); } return null; } async function fetchBilibiliMetrics(bvid: string): Promise { 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 { 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(), }; }