feat: add the view graph on tiktok
This commit is contained in:
@@ -17,6 +17,7 @@ const prisma = new PrismaClient({ adapter });
|
||||
|
||||
const TIKTOK_TOKEN_URL = "https://open.tiktokapis.com/v2/oauth/token/";
|
||||
const TIKTOK_USER_INFO_URL = "https://open.tiktokapis.com/v2/user/info/";
|
||||
const TIKTOK_VIDEO_LIST_URL = "https://open.tiktokapis.com/v2/video/list/";
|
||||
const CLIENT_KEY = process.env.TIKTOK_CLIENT_KEY!;
|
||||
const CLIENT_SECRET = process.env.TIKTOK_CLIENT_SECRET!;
|
||||
const INTERVAL_MS = 60 * 60 * 1000; // 1 heure
|
||||
@@ -57,7 +58,7 @@ async function refreshTikTokToken(refreshTokenStr: string) {
|
||||
}
|
||||
|
||||
async function fetchTikTokStats(accessToken: string) {
|
||||
const fields = "follower_count,likes_count,video_count,display_name";
|
||||
const fields = "follower_count,likes_count,video_count,video_view_count,display_name";
|
||||
const res = await fetch(`${TIKTOK_USER_INFO_URL}?fields=${fields}`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
@@ -68,14 +69,66 @@ async function fetchTikTokStats(accessToken: string) {
|
||||
}
|
||||
|
||||
const user = data.data?.user ?? {};
|
||||
let views = (user.video_view_count ?? user.profile_view_count ?? 0) as number;
|
||||
|
||||
if (user.video_view_count == null) {
|
||||
try {
|
||||
views = await fetchTotalVideoViews(accessToken);
|
||||
} catch (err) {
|
||||
console.warn("[worker] fallback views failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
followers: (user.follower_count ?? 0) as number,
|
||||
likes: (user.likes_count ?? 0) as number,
|
||||
videoCount: (user.video_count ?? 0) as number,
|
||||
views,
|
||||
displayName: (user.display_name ?? "") as string,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchTotalVideoViews(accessToken: string): Promise<number> {
|
||||
const fields = "id,view_count";
|
||||
let cursor = 0;
|
||||
let totalViews = 0;
|
||||
|
||||
for (let page = 0; page < 50; page++) {
|
||||
const res = await fetch(`${TIKTOK_VIDEO_LIST_URL}?fields=${fields}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ max_count: 20, cursor }),
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok || data.error?.code !== "ok") {
|
||||
throw new Error(data.error?.message ?? "Video list fetch failed");
|
||||
}
|
||||
|
||||
const videos = Array.isArray(data.data?.videos) ? data.data.videos : [];
|
||||
for (const video of videos) {
|
||||
totalViews += Number(video?.view_count ?? 0);
|
||||
}
|
||||
|
||||
const hasMore = Boolean(data.data?.has_more);
|
||||
if (!hasMore) {
|
||||
break;
|
||||
}
|
||||
|
||||
const nextCursor = Number(data.data?.cursor ?? cursor + videos.length);
|
||||
if (!Number.isFinite(nextCursor) || nextCursor <= cursor) {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = nextCursor;
|
||||
}
|
||||
|
||||
return totalViews;
|
||||
}
|
||||
|
||||
// ── Core job ──────────────────────────────────────────────────
|
||||
|
||||
async function runSnapshots() {
|
||||
@@ -136,11 +189,11 @@ async function runSnapshots() {
|
||||
followers: stats.followers,
|
||||
likes: stats.likes,
|
||||
videoCount: stats.videoCount,
|
||||
views: 0,
|
||||
views: stats.views,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`[worker] ✓ userId=${userId} — followers=${stats.followers} likes=${stats.likes} videos=${stats.videoCount}`);
|
||||
console.log(`[worker] ✓ userId=${userId} — followers=${stats.followers} likes=${stats.likes} videos=${stats.videoCount} views=${stats.views}`);
|
||||
|
||||
} catch (err) {
|
||||
console.error(`[worker] ✗ erreur userId=${userId}:`, err);
|
||||
|
||||
Reference in New Issue
Block a user