From 3dfed67539dc6b17477aa9b9ed8314cb6957a8e4 Mon Sep 17 00:00:00 2001 From: knuthtimo-lab Date: Sat, 11 Jul 2026 22:34:31 +0200 Subject: [PATCH] TikTok V6 --- server/index.js | 49 +++++++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/server/index.js b/server/index.js index 57a84fd..8453ad9 100644 --- a/server/index.js +++ b/server/index.js @@ -1225,7 +1225,7 @@ app.get('/api/tiktok/connect', (request, response) => { const authUrl = new URL('https://www.tiktok.com/v2/auth/authorize/'); authUrl.searchParams.set('client_key', clientKey); - authUrl.searchParams.set('scope', 'user.info.basic,user.info.stats,video.publish,video.upload,video.list'); + authUrl.searchParams.set('scope', 'user.info.basic,video.publish,video.upload'); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('redirect_uri', redirectUri); authUrl.searchParams.set('state', oauthState); @@ -1571,11 +1571,12 @@ app.get('/api/tiktok/upload/status', async (request, response) => { }); // ─── TikTok Analytics ─────────────────────────────────────────────────────── -// Live read-only stats from the Display API. Requires the user.info.stats and -// video.list scopes — accounts connected before the scope change must -// re-authorize via /api/tiktok/connect. +// user.info only requests fields covered by the approved user.info.basic +// scope. video.list needs a scope GreenLens's TikTok app isn't approved for +// (would require adding the Display API product and a new app review), so +// video stats are omitted until that's added. -const TIKTOK_USER_FIELDS = 'display_name,follower_count,following_count,likes_count,video_count'; +const TIKTOK_USER_FIELDS = 'display_name,avatar_url'; const TIKTOK_VIDEO_FIELDS = 'id,title,video_description,duration,create_time,share_url,view_count,like_count,comment_count,share_count'; const TIKTOK_VIDEO_PAGE_SIZE = 20; // Display API maximum per page @@ -1634,21 +1635,32 @@ app.get('/api/tiktok/analytics', async (request, response) => { ); const videos = []; - let cursor; - let hasMore = true; - while (hasMore && videos.length < maxVideos) { - const pageBody = { max_count: Math.min(TIKTOK_VIDEO_PAGE_SIZE, maxVideos - videos.length) }; - if (cursor) pageBody.cursor = cursor; + let videoListUnavailable = false; + try { + let cursor; + let hasMore = true; + while (hasMore && videos.length < maxVideos) { + const pageBody = { max_count: Math.min(TIKTOK_VIDEO_PAGE_SIZE, maxVideos - videos.length) }; + if (cursor) pageBody.cursor = cursor; - const page = await tiktokApi( - `https://open.tiktokapis.com/v2/video/list/?fields=${TIKTOK_VIDEO_FIELDS}`, - { method: 'POST', body: Buffer.from(JSON.stringify(pageBody)) }, - ); + const page = await tiktokApi( + `https://open.tiktokapis.com/v2/video/list/?fields=${TIKTOK_VIDEO_FIELDS}`, + { method: 'POST', body: Buffer.from(JSON.stringify(pageBody)) }, + ); - const pageVideos = Array.isArray(page?.data?.videos) ? page.data.videos : []; - videos.push(...pageVideos); - cursor = page?.data?.cursor; - hasMore = Boolean(page?.data?.has_more) && pageVideos.length > 0; + const pageVideos = Array.isArray(page?.data?.videos) ? page.data.videos : []; + videos.push(...pageVideos); + cursor = page?.data?.cursor; + hasMore = Boolean(page?.data?.has_more) && pageVideos.length > 0; + } + } catch (error) { + // video.list is not part of GreenLens's approved TikTok app scopes yet; + // still return the confirmed account identity from user.info. + if (error.body?.error?.code === 'scope_not_authorized') { + videoListUnavailable = true; + } else { + throw error; + } } const { videos: enrichedVideos, summary } = summarizeTiktokVideos(videos); @@ -1657,6 +1669,7 @@ app.get('/api/tiktok/analytics', async (request, response) => { user: userResult?.data?.user || null, summary, videos: enrichedVideos, + videoListUnavailable, }); } catch (error) { const payload = toApiErrorPayload(error);