From d8437ae39a30baf314e363a644d9bfdba072d358 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Fri, 3 Jul 2026 23:50:56 +0700 Subject: [PATCH] refactor: migrate TikTok polling from external cron to internal Next.js instrumentation interval --- docker-compose.yml | 7 --- instrumentation.ts | 6 +++ .../cron/tiktok/route.ts => lib/autofeed.ts | 47 ++++++++----------- lib/followers/tiktok.ts | 11 +---- 4 files changed, 26 insertions(+), 45 deletions(-) create mode 100644 instrumentation.ts rename app/api/cron/tiktok/route.ts => lib/autofeed.ts (52%) diff --git a/docker-compose.yml b/docker-compose.yml index dcf400c..9462f57 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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" diff --git a/instrumentation.ts b/instrumentation.ts new file mode 100644 index 0000000..72eb8e7 --- /dev/null +++ b/instrumentation.ts @@ -0,0 +1,6 @@ +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + const { startTiktokFeedInterval } = await import('@/lib/autofeed'); + startTiktokFeedInterval(); + } +} diff --git a/app/api/cron/tiktok/route.ts b/lib/autofeed.ts similarity index 52% rename from app/api/cron/tiktok/route.ts rename to lib/autofeed.ts index f81b725..57f710c 100644 --- a/app/api/cron/tiktok/route.ts +++ b/lib/autofeed.ts @@ -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); } } diff --git a/lib/followers/tiktok.ts b/lib/followers/tiktok.ts index 855448b..6163b5a 100644 --- a/lib/followers/tiktok.ts +++ b/lib/followers/tiktok.ts @@ -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; @@ -143,4 +134,4 @@ export async function getLatestTiktokVideo(): Promise { console.error("[TikTok API] Error fetching latest video via TikWM:", e); return null; } -} +} \ No newline at end of file