TikTok V6
This commit is contained in:
@@ -1225,7 +1225,7 @@ app.get('/api/tiktok/connect', (request, response) => {
|
|||||||
|
|
||||||
const authUrl = new URL('https://www.tiktok.com/v2/auth/authorize/');
|
const authUrl = new URL('https://www.tiktok.com/v2/auth/authorize/');
|
||||||
authUrl.searchParams.set('client_key', clientKey);
|
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('response_type', 'code');
|
||||||
authUrl.searchParams.set('redirect_uri', redirectUri);
|
authUrl.searchParams.set('redirect_uri', redirectUri);
|
||||||
authUrl.searchParams.set('state', oauthState);
|
authUrl.searchParams.set('state', oauthState);
|
||||||
@@ -1571,11 +1571,12 @@ app.get('/api/tiktok/upload/status', async (request, response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ─── TikTok Analytics ───────────────────────────────────────────────────────
|
// ─── TikTok Analytics ───────────────────────────────────────────────────────
|
||||||
// Live read-only stats from the Display API. Requires the user.info.stats and
|
// user.info only requests fields covered by the approved user.info.basic
|
||||||
// video.list scopes — accounts connected before the scope change must
|
// scope. video.list needs a scope GreenLens's TikTok app isn't approved for
|
||||||
// re-authorize via /api/tiktok/connect.
|
// (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_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
|
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 = [];
|
const videos = [];
|
||||||
let cursor;
|
let videoListUnavailable = false;
|
||||||
let hasMore = true;
|
try {
|
||||||
while (hasMore && videos.length < maxVideos) {
|
let cursor;
|
||||||
const pageBody = { max_count: Math.min(TIKTOK_VIDEO_PAGE_SIZE, maxVideos - videos.length) };
|
let hasMore = true;
|
||||||
if (cursor) pageBody.cursor = cursor;
|
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(
|
const page = await tiktokApi(
|
||||||
`https://open.tiktokapis.com/v2/video/list/?fields=${TIKTOK_VIDEO_FIELDS}`,
|
`https://open.tiktokapis.com/v2/video/list/?fields=${TIKTOK_VIDEO_FIELDS}`,
|
||||||
{ method: 'POST', body: Buffer.from(JSON.stringify(pageBody)) },
|
{ method: 'POST', body: Buffer.from(JSON.stringify(pageBody)) },
|
||||||
);
|
);
|
||||||
|
|
||||||
const pageVideos = Array.isArray(page?.data?.videos) ? page.data.videos : [];
|
const pageVideos = Array.isArray(page?.data?.videos) ? page.data.videos : [];
|
||||||
videos.push(...pageVideos);
|
videos.push(...pageVideos);
|
||||||
cursor = page?.data?.cursor;
|
cursor = page?.data?.cursor;
|
||||||
hasMore = Boolean(page?.data?.has_more) && pageVideos.length > 0;
|
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);
|
const { videos: enrichedVideos, summary } = summarizeTiktokVideos(videos);
|
||||||
@@ -1657,6 +1669,7 @@ app.get('/api/tiktok/analytics', async (request, response) => {
|
|||||||
user: userResult?.data?.user || null,
|
user: userResult?.data?.user || null,
|
||||||
summary,
|
summary,
|
||||||
videos: enrichedVideos,
|
videos: enrichedVideos,
|
||||||
|
videoListUnavailable,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const payload = toApiErrorPayload(error);
|
const payload = toApiErrorPayload(error);
|
||||||
|
|||||||
Reference in New Issue
Block a user