refactor: migrate TikTok polling from external cron to internal Next.js instrumentation interval

This commit is contained in:
2026-07-03 23:50:56 +07:00 Unverified
parent 46360826b6
commit d8437ae39a
4 changed files with 26 additions and 45 deletions
-7
View File
@@ -14,10 +14,3 @@ services:
- erika
restart: unless-stopped
cron:
image: curlimages/curl
network_mode: host
restart: unless-stopped
env_file:
- .env
command: /bin/sh -c "while true; do echo \"[$$(date)] Triggering TikTok cron...\"; curl -s \"http://localhost:3000/api/cron/tiktok?secret=$${CRON_SECRET}\"; echo \"\"; sleep 300; done"
+6
View File
@@ -0,0 +1,6 @@
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { startTiktokFeedInterval } = await import('@/lib/autofeed');
startTiktokFeedInterval();
}
}
@@ -1,35 +1,20 @@
import { NextResponse } from "next/server";
import { getLatestTiktokVideo, getTiktokProfile } from "@/lib/followers/tiktok";
import { getRedisClient } from "@/lib/redis";
import { sendDiscordNotification } from "@/lib/discord";
import { getLatestTiktokVideo, getTiktokProfile } from "@/lib/followers/tiktok";
export async function GET(req: Request) {
async function checkTiktokFeed() {
try {
const { searchParams } = new URL(req.url);
// 1. Verify cron secret
const cronSecret = process.env.CRON_SECRET;
if (!cronSecret || searchParams.get("secret") !== cronSecret) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// 2. Fetch the latest video from TikTok API
const latestVideo = await getLatestTiktokVideo();
if (!latestVideo) {
return NextResponse.json({ message: "No videos found or failed to fetch." }, { status: 200 });
}
if (!latestVideo) return;
// 3. Check Redis to see if we've already notified about this video
const redis = await getRedisClient();
const redisKey = "erika:tiktok_last_video_id";
const lastVideoId = await redis.get(redisKey);
if (lastVideoId === latestVideo.id) {
// Already notified
return NextResponse.json({ message: "No new videos.", id: latestVideo.id }, { status: 200 });
return; // Already notified
}
// 4. We have a new video! Prepare the notification
let officialAuthorName = "TikTok";
let officialAvatarUrl: string | undefined = undefined;
try {
@@ -37,7 +22,7 @@ export async function GET(req: Request) {
if (profile.displayName) officialAuthorName = profile.displayName;
if (profile.avatarUrl) officialAvatarUrl = profile.avatarUrl;
} catch (e) {
console.error("[TikTok Cron] Failed to fetch profile:", e);
console.error("[TikTok Feed] Failed to fetch profile:", e);
}
const message = `ลงคลิปใหม่แล้วววสสสสส ${latestVideo.shareUrl}`;
@@ -53,16 +38,22 @@ export async function GET(req: Request) {
image: latestVideo.coverImageUrl ? { url: latestVideo.coverImageUrl } : undefined
};
// 5. Send Discord notification
await sendDiscordNotification(message, embed);
// 6. Update Redis with the new video ID
// We don't set an expiry because we want to remember it forever (until the next video)
await redis.set(redisKey, latestVideo.id);
return NextResponse.json({ success: true, message: "New video notified!", id: latestVideo.id }, { status: 200 });
} catch (error) {
console.error("[TikTok Cron] Error processing request:", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
console.error("[TikTok Feed] Error processing:", error);
}
}
export function startTiktokFeedInterval() {
if (!(global as any).tiktokFeedIntervalStarted) {
(global as any).tiktokFeedIntervalStarted = true;
console.log("[TikTok Feed] Starting 5-minute interval check in background...");
// Run immediately on start
checkTiktokFeed();
// Then every 5 minutes
setInterval(checkTiktokFeed, 5 * 60 * 1000);
}
}
-9
View File
@@ -1,12 +1,3 @@
/**
* Fetches TikTok follower count using the official TikTok API v2.
*
* Required env vars:
* - TIKTOK_CLIENT_KEY
* - TIKTOK_CLIENT_SECRET
* - TIKTOK_REFRESH_TOKEN (obtained via one-time OAuth flow at /api/auth/tiktok)
*/
let cachedAccessToken: string | null = null;
let tokenExpiresAt = 0;