TikTok V6
This commit is contained in:
@@ -120,14 +120,54 @@ const downloadRemotePhoto = async (sourceUrl) => {
|
||||
}
|
||||
};
|
||||
|
||||
const sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Preflight before TikTok ever sees a URL: it must answer 200 without a
|
||||
// redirect, be JPEG or WebP, and stay within TikTok's 20 MiB limit. Also
|
||||
// serves as the read-after-write check right after a MinIO upload. Retries a
|
||||
// couple of times to ride out brief 404/5xx windows after a fresh write.
|
||||
const verifyPublicPhotoUrl = async (url, { attempts = 3, fetchImpl = fetch } = {}) => {
|
||||
let lastReason = 'unreachable';
|
||||
for (let attempt = 0; attempt < attempts; attempt++) {
|
||||
if (attempt > 0) await sleepMs(1000 * attempt);
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10_000);
|
||||
try {
|
||||
const response = await fetchImpl(url, { method: 'HEAD', redirect: 'error', signal: controller.signal });
|
||||
if (response.status !== 200) {
|
||||
lastReason = `HTTP ${response.status}`;
|
||||
continue;
|
||||
}
|
||||
const contentType = String(response.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
|
||||
if (!['image/jpeg', 'image/webp'].includes(contentType)) {
|
||||
lastReason = `unsupported content type ${contentType || '(none)'}`;
|
||||
break;
|
||||
}
|
||||
const size = Number(response.headers.get('content-length') || 0);
|
||||
if (size > MAX_TIKTOK_PHOTO_BYTES) {
|
||||
lastReason = 'larger than 20 MB';
|
||||
break;
|
||||
}
|
||||
return;
|
||||
} catch {
|
||||
lastReason = 'unreachable or redirected';
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
throw createInputError(`Photo URL is not usable for TikTok (${lastReason}): ${url}`);
|
||||
};
|
||||
|
||||
const prepareTiktokPhotoUrls = async (photos, options) => {
|
||||
const publicBaseUrl = normalizePublicBaseUrl(options.publicBaseUrl);
|
||||
if (!publicBaseUrl) throw new Error('MINIO_PUBLIC_URL is not configured.');
|
||||
const verifyUrl = options.verifyPhotoUrl || verifyPublicPhotoUrl;
|
||||
|
||||
const urls = [];
|
||||
for (const photo of photos) {
|
||||
const sourceUrl = typeof photo === 'string' ? photo.trim() : String(photo?.url || '').trim();
|
||||
if (sourceUrl && isOwnedTiktokMediaUrl(sourceUrl, publicBaseUrl)) {
|
||||
await verifyUrl(sourceUrl);
|
||||
urls.push(sourceUrl);
|
||||
continue;
|
||||
}
|
||||
@@ -145,6 +185,7 @@ const prepareTiktokPhotoUrls = async (photos, options) => {
|
||||
if (!isOwnedTiktokMediaUrl(uploaded?.url, publicBaseUrl)) {
|
||||
throw new Error('GreenLens image storage returned an unverified public URL.');
|
||||
}
|
||||
await verifyUrl(uploaded.url);
|
||||
urls.push(uploaded.url);
|
||||
}
|
||||
|
||||
@@ -155,5 +196,6 @@ module.exports = {
|
||||
MAX_TIKTOK_PHOTO_BYTES,
|
||||
isAllowedR2MediaUrl,
|
||||
isOwnedTiktokMediaUrl,
|
||||
verifyPublicPhotoUrl,
|
||||
prepareTiktokPhotoUrls,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user