imagex-upload.js
1 /** 2 * ImageX cover image uploader. 3 * 4 * Uploads a JPEG/PNG image to ByteDance ImageX via a pre-signed PUT URL 5 * obtained from the Douyin "apply cover upload" API. 6 */ 7 import * as fs from 'node:fs'; 8 import * as path from 'node:path'; 9 import { CommandExecutionError } from '@jackwener/opencli/errors'; 10 /** 11 * Detect MIME type from file extension. 12 * Falls back to image/jpeg for unknown extensions. 13 */ 14 function detectContentType(filePath) { 15 const ext = path.extname(filePath).toLowerCase(); 16 switch (ext) { 17 case '.png': 18 return 'image/png'; 19 case '.gif': 20 return 'image/gif'; 21 case '.webp': 22 return 'image/webp'; 23 default: 24 return 'image/jpeg'; 25 } 26 } 27 /** 28 * Upload a cover image to ByteDance ImageX via a pre-signed PUT URL. 29 * 30 * @param imagePath - Local file path to the image (JPEG/PNG/etc.) 31 * @param uploadInfo - Upload URL and store_uri from the apply cover upload API 32 * @returns The store_uri (= image_uri for use in create_v2) 33 */ 34 export async function imagexUpload(imagePath, uploadInfo) { 35 if (!fs.existsSync(imagePath)) { 36 throw new CommandExecutionError(`Cover image file not found: ${imagePath}`, 'Ensure the file path is correct and accessible.'); 37 } 38 const imageBuffer = fs.readFileSync(imagePath); 39 const contentType = detectContentType(imagePath); 40 const res = await fetch(uploadInfo.upload_url, { 41 method: 'PUT', 42 headers: { 43 'Content-Type': contentType, 44 'Content-Length': String(imageBuffer.byteLength), 45 }, 46 body: imageBuffer, 47 }); 48 if (!res.ok) { 49 const body = await res.text().catch(() => ''); 50 throw new CommandExecutionError(`ImageX upload failed with status ${res.status}: ${body}`, 'Check that the upload URL is valid and has not expired.'); 51 } 52 return uploadInfo.store_uri; 53 }