Bild Carousel

This commit is contained in:
2026-07-09 16:45:03 +02:00
parent 35f3ed0d0e
commit 2a14c84ad3
8 changed files with 289385 additions and 2 deletions

View File

@@ -24,7 +24,7 @@ export async function GET(request: NextRequest) {
const authUrl = new URL('https://www.tiktok.com/v2/auth/authorize/');
authUrl.searchParams.set('client_key', clientKey);
authUrl.searchParams.set('scope', 'user.info.basic,video.upload');
authUrl.searchParams.set('scope', 'user.info.basic,video.upload,video.publish');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('redirect_uri', redirectUri);
authUrl.searchParams.set('state', oauthState);

View File

@@ -0,0 +1,145 @@
import { NextRequest, NextResponse } from 'next/server';
import { getValidTiktokTokens, getLiveTiktokAccessToken, tiktokApi, uploadBinaryToTiktok } from '@/lib/tiktok';
const isAdminRequest = (request: NextRequest) => {
const adminKey = process.env.TIKTOK_ADMIN_KEY;
if (!adminKey) return false;
const provided =
request.headers.get('x-admin-key') || request.nextUrl.searchParams.get('key');
return provided === adminKey;
};
export async function POST(request: NextRequest) {
if (!isAdminRequest(request)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const tokens = await getValidTiktokTokens();
if (!tokens) {
return NextResponse.json(
{ error: 'No TikTok account connected. Visit /api/tiktok/connect first.' },
{ status: 404 }
);
}
const contentType = request.headers.get('content-type') || '';
let publishId: string | undefined;
let status = 'INITIATED';
if (contentType.includes('application/json')) {
const json = await request.json().catch(() => ({}));
if (typeof json?.videoBufferBase64 === 'string') {
const videoBuffer = Buffer.from(json.videoBufferBase64, 'base64');
const videoSize = videoBuffer.length;
const initBody = {
source_info: {
source: 'FILE_UPLOAD',
video_size: videoSize,
chunk_size: videoSize,
total_chunk_count: 1,
},
};
const initResult = await tiktokApi(
'https://open.tiktokapis.com/v2/post/publish/inbox/video/init/',
{
method: 'POST',
body: JSON.stringify(initBody),
}
);
const uploadUrl = initResult?.data?.upload_url as string | undefined;
publishId = initResult?.data?.publish_id as string | undefined;
if (!uploadUrl || !publishId) {
return NextResponse.json(
{ error: 'Missing upload_url or publish_id from TikTok.' },
{ status: 502 }
);
}
await uploadBinaryToTiktok(uploadUrl, videoBuffer, 'video/mp4');
} else if (Array.isArray(json?.photos)) {
const photos = json.photos as Array<{ url?: string } | string>;
if (!photos.length) {
return NextResponse.json(
{ error: 'photos must be a non-empty array for photo uploads.' },
{ status: 400 }
);
}
if (photos.length > 35) {
return NextResponse.json(
{ error: 'TikTok allows up to 35 photos per carousel post.' },
{ status: 400 }
);
}
const photoUrls = photos
.map((photo) => {
if (typeof photo === 'string') return photo.trim();
return String(photo?.url || '').trim();
})
.filter(Boolean);
if (!photoUrls.length) {
return NextResponse.json(
{ error: 'Each photo entry must include a public url.' },
{ status: 400 }
);
}
const initBody = {
media_type: 'PHOTO',
photo_cover_index: 0,
file_paths: photoUrls,
file_extensions: photoUrls.map((url) => {
const fileName = String(new URL(url).pathname).split('/').pop() || 'photo.jpg';
const extension = fileName.split('.').pop() || 'jpg';
return extension.startsWith('.') ? extension.slice(1) : extension;
}),
post_mode: 'DIRECT_POST',
};
const initResult = await tiktokApi(
'https://open.tiktokapis.com/v2/post/publish/content/init/',
{
method: 'POST',
body: JSON.stringify(initBody),
}
);
publishId = initResult?.data?.publish_id as string | undefined;
status = (initResult?.data?.status as string) || 'INITIATED';
} else {
return NextResponse.json(
{ error: 'Unsupported JSON payload. Use videoBufferBase64 or photos.' },
{ status: 400 }
);
}
} else {
return NextResponse.json(
{ error: 'Unsupported content type. Use application/json for TikTok uploads.' },
{ status: 415 }
);
}
if (!publishId) {
return NextResponse.json(
{ error: 'Missing publish_id from TikTok.' },
{ status: 502 }
);
}
return NextResponse.json({
publish_id: publishId,
status,
});
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
const status = err instanceof Error && 'status' in err ? (err as { status?: number }).status : 502;
return NextResponse.json({ error: message }, { status: status || 502 });
}
}

View File

@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from 'next/server';
import { tiktokApi } from '@/lib/tiktok';
export async function GET(request: NextRequest) {
const publishId = String(request.nextUrl.searchParams.get('publish_id') || '').trim();
if (!publishId) {
return NextResponse.json({ error: 'publish_id is required.' }, { status: 400 });
}
try {
const result = await tiktokApi('https://open.tiktokapis.com/v2/post/publish/status/fetch/', {
method: 'POST',
body: JSON.stringify({ publish_id: publishId }),
});
return NextResponse.json(result);
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
const status = err instanceof Error && 'status' in err ? (err as { status?: number }).status : 502;
return NextResponse.json({ error: message }, { status: status || 502 });
}
}