UNPKG

upload-post

Version:

Official client library for Upload-Post API - cross-platform social media video upload

345 lines (307 loc) 17.5 kB
import axios from 'axios'; import FormData from 'form-data'; import fs from 'fs'; import { createReadStream } from 'fs'; /** * @typedef {Object} UploadOptions * @property {string} title - Video title * @property {string} user - User identifier * @property {string[]} platforms - Array of platforms (e.g. ['tiktok', 'youtube', 'instagram']) * @property {string} [video] - The video file to upload (can be a file upload or a video URL) - typically handled by videoPath param. * * @property {'PUBLIC_TO_EVERYONE' | 'MUTUAL_FOLLOW_FRIENDS' | 'FOLLOWER_OF_CREATOR' | 'SELF_ONLY'} [privacy_level] - TikTok: Privacy setting * @property {boolean} [disable_duet] - TikTok: Disable duet feature * @property {boolean} [disable_comment] - TikTok: Disable comments * @property {boolean} [disable_stitch] - TikTok: Disable stitch feature * @property {number} [cover_timestamp] - TikTok: Timestamp in milliseconds for video cover * @property {boolean} [brand_content_toggle] - TikTok: Enable branded content * @property {boolean} [brand_organic] - TikTok: Enable organic branded content * @property {boolean} [branded_content] - TikTok: Enable branded content with disclosure * @property {boolean} [brand_organic_toggle] - TikTok: Enable organic branded content toggle * @property {boolean} [is_aigc] - TikTok: Indicates if content is AI-generated * * @property {'REELS' | 'STORIES'} [media_type] - Instagram: Type of media * @property {boolean} [share_to_feed] - Instagram: Whether to share to feed * @property {string} [collaborators] - Instagram: Comma-separated list of collaborator usernames * @property {string} [cover_url] - Instagram: URL for custom video cover * @property {string} [audio_name] - Instagram: Name of the audio track * @property {string} [user_tags] - Instagram: Comma-separated list of user tags * @property {string} [location_id] - Instagram: Instagram location ID * @property {string} [thumb_offset] - Instagram: Timestamp offset for video thumbnail * * @property {string} [description] - LinkedIn, YouTube, Facebook, Threads: Description or commentary * @property {'CONNECTIONS' | 'PUBLIC' | 'LOGGED_IN' | 'CONTAINER'} [visibility] - LinkedIn: Visibility setting * @property {string} [target_linkedin_page_id] - LinkedIn: Page ID for organization uploads * * @property {string[]} [tags] - YouTube: Array of tags * @property {string} [categoryId] - YouTube: Video category ID * @property {'public' | 'unlisted' | 'private'} [privacyStatus] - YouTube: Privacy setting * @property {boolean} [embeddable] - YouTube: Whether video is embeddable * @property {'youtube' | 'creativeCommon'} [license] - YouTube: Video license * @property {boolean} [publicStatsViewable] - YouTube: Whether public stats are viewable * @property {boolean} [madeForKids] - YouTube: Whether video is made for kids * * @property {string} [facebook_page_id] - Facebook: Page ID * @property {'DRAFT' | 'PUBLISHED' | 'SCHEDULED'} [video_state] - Facebook: Desired state of the video * * @property {string[]} [tagged_user_ids] - X (Twitter): Array of user IDs to tag * @property {'following' | 'mentionedUsers' | 'everyone'} [reply_settings] - X (Twitter): Who can reply * @property {boolean} [nullcast] - X (Twitter): Whether to publish without broadcasting * @property {string} [place_id] - X (Twitter): Location place ID * @property {number} [poll_duration] - X (Twitter): Poll duration in minutes * @property {string[]} [poll_options] - X (Twitter): Array of poll options * @property {'following' | 'mentionedUsers' | 'everyone'} [poll_reply_settings] - X (Twitter): Who can reply to poll * * @property {string} [pinterest_board_id] - Pinterest: Board ID * @property {string} [pinterest_link] - Pinterest: Destination link * @property {string} [pinterest_cover_image_url] - Pinterest: URL of an image to use as the video cover * @property {string} [pinterest_cover_image_content_type] - Pinterest: Content type of the cover image * @property {string} [pinterest_cover_image_data] - Pinterest: Base64 encoded cover image data * @property {number} [pinterest_cover_image_key_frame_time] - Pinterest: Time in ms of video frame for cover */ /** * @typedef {Object} UploadPhotosOptions * @property {string} user - User identifier * @property {string[]} platforms - Array of target platforms. Supported: tiktok, instagram, linkedin, facebook, x, threads, pinterest * @property {string[]} photos - Array of photo file paths or URLs * @property {string} title - Title of the post * @property {string} [caption] - Caption/description for the photos * @property {'PUBLIC'} [visibility] - LinkedIn: Visibility setting ("PUBLIC") * @property {string} [target_linkedin_page_id] - LinkedIn: Page ID for organization uploads * @property {string} [facebook_page_id] - Facebook: Page ID * @property {boolean} [auto_add_music] - TikTok: Automatically add background music * @property {boolean} [disable_comment] - TikTok: Disable comments * @property {boolean} [branded_content] - TikTok: Branded content (requires disclose_commercial=true) * @property {boolean} [disclose_commercial] - TikTok: Disclose commercial nature * @property {number} [photo_cover_index] - TikTok: Index of photo for cover * @property {string} [description] - TikTok: Description (defaults to title) * @property {'IMAGE' | 'STORIES'} [media_type] - Instagram: Media type ("IMAGE" or "STORIES") * @property {string} [pinterest_board_id] - Pinterest: Board ID * @property {string} [pinterest_alt_text] - Pinterest: Alt text * @property {string} [pinterest_link] - Pinterest: Destination link */ /** * @typedef {Object} UploadTextOptions * @property {string} user - User identifier * @property {('linkedin' | 'x' | 'facebook' | 'threads')[]} platforms - Array of target platforms. * @property {string} title - The text content for the post. * @property {string} [target_linkedin_page_id] - LinkedIn: Page ID for organization posts. * @property {string} [facebook_page_id] - Facebook: Page ID. Required if 'facebook' is in platforms. */ /** * Upload-Post API client */ export class UploadPost { /** * @param {string} apiKey - Your API key from Upload-Post */ constructor(apiKey) { this.apiKey = apiKey; this.baseUrl = 'https://api.upload-post.com/api'; } /** * Upload video to social media platforms * @param {string} videoPath - Path to video file * @param {UploadOptions} options - Upload options * @returns {Promise<Object>} API response */ async upload(videoPath, options) { const form = new FormData(); form.append('video', createReadStream(videoPath)); form.append('title', options.title); form.append('user', options.user); options.platforms.forEach(platform => { form.append('platform[]', platform); }); // Add platform-specific parameters if they exist in options // TikTok if (options.privacy_level) form.append('privacy_level', options.privacy_level); if (options.disable_duet !== undefined) form.append('disable_duet', options.disable_duet.toString()); if (options.disable_comment !== undefined) form.append('disable_comment', options.disable_comment.toString()); if (options.disable_stitch !== undefined) form.append('disable_stitch', options.disable_stitch.toString()); if (options.cover_timestamp !== undefined) form.append('cover_timestamp', options.cover_timestamp.toString()); if (options.brand_content_toggle !== undefined) form.append('brand_content_toggle', options.brand_content_toggle.toString()); if (options.brand_organic !== undefined) form.append('brand_organic', options.brand_organic.toString()); if (options.branded_content !== undefined) form.append('branded_content', options.branded_content.toString()); if (options.brand_organic_toggle !== undefined) form.append('brand_organic_toggle', options.brand_organic_toggle.toString()); if (options.is_aigc !== undefined) form.append('is_aigc', options.is_aigc.toString()); // Instagram if (options.media_type) form.append('media_type', options.media_type); // Used by photos and videos if (options.share_to_feed !== undefined) form.append('share_to_feed', options.share_to_feed.toString()); if (options.collaborators) form.append('collaborators', options.collaborators); if (options.cover_url) form.append('cover_url', options.cover_url); if (options.audio_name) form.append('audio_name', options.audio_name); if (options.user_tags) form.append('user_tags', options.user_tags); if (options.location_id) form.append('location_id', options.location_id); if (options.thumb_offset) form.append('thumb_offset', options.thumb_offset); // LinkedIn if (options.description) form.append('description', options.description); // Used by multiple platforms if (options.visibility) form.append('visibility', options.visibility); // Used by multiple platforms if (options.target_linkedin_page_id) form.append('target_linkedin_page_id', options.target_linkedin_page_id); // YouTube if (options.tags) options.tags.forEach(tag => form.append('tags[]', tag)); if (options.categoryId) form.append('categoryId', options.categoryId); if (options.privacyStatus) form.append('privacyStatus', options.privacyStatus); if (options.embeddable !== undefined) form.append('embeddable', options.embeddable.toString()); if (options.license) form.append('license', options.license); if (options.publicStatsViewable !== undefined) form.append('publicStatsViewable', options.publicStatsViewable.toString()); if (options.madeForKids !== undefined) form.append('madeForKids', options.madeForKids.toString()); // Facebook if (options.facebook_page_id) form.append('facebook_page_id', options.facebook_page_id); if (options.video_state) form.append('video_state', options.video_state); // X (Twitter) if (options.tagged_user_ids) options.tagged_user_ids.forEach(id => form.append('tagged_user_ids[]', id)); if (options.reply_settings) form.append('reply_settings', options.reply_settings); if (options.nullcast !== undefined) form.append('nullcast', options.nullcast.toString()); if (options.place_id) form.append('place_id', options.place_id); if (options.poll_duration !== undefined) form.append('poll_duration', options.poll_duration.toString()); if (options.poll_options) options.poll_options.forEach(opt => form.append('poll_options[]', opt)); if (options.poll_reply_settings) form.append('poll_reply_settings', options.poll_reply_settings); // Pinterest if (options.pinterest_board_id) form.append('pinterest_board_id', options.pinterest_board_id); if (options.pinterest_link) form.append('pinterest_link', options.pinterest_link); if (options.pinterest_cover_image_url) form.append('pinterest_cover_image_url', options.pinterest_cover_image_url); if (options.pinterest_cover_image_content_type) form.append('pinterest_cover_image_content_type', options.pinterest_cover_image_content_type); if (options.pinterest_cover_image_data) form.append('pinterest_cover_image_data', options.pinterest_cover_image_data); if (options.pinterest_cover_image_key_frame_time !== undefined) form.append('pinterest_cover_image_key_frame_time', options.pinterest_cover_image_key_frame_time.toString()); // If video is passed as a URL in options.video (as per API docs for URL uploads) if (options.video && (options.video.startsWith('http://') || options.video.startsWith('https://'))) { form.append('video', options.video); } try { const response = await axios.post(`${this.baseUrl}/upload`, form, { headers: { ...form.getHeaders(), 'Authorization': `Apikey ${this.apiKey}` }, maxContentLength: Infinity, maxBodyLength: Infinity }); if (response.status !== 200) { throw new Error(`API request failed with status ${response.status}`); } return response.data; } catch (error) { throw new Error(`Upload failed: ${error.message}`); } } /** * Upload photos to social media platforms * @param {string[]} photosPathsOrUrls - Array of photo file paths or URLs * @param {UploadPhotosOptions} options - Upload photos options * @returns {Promise<Object>} API response */ async uploadPhotos(photosPathsOrUrls, options) { const form = new FormData(); // Common parameters form.append('user', options.user); form.append('title', options.title); if (options.caption) { form.append('caption', options.caption); } options.platforms.forEach(platform => { form.append('platform[]', platform); }); photosPathsOrUrls.forEach(photoItem => { if (typeof photoItem === 'string' && (photoItem.startsWith('http://') || photoItem.startsWith('https://'))) { form.append('photos[]', photoItem); // Pass URL as string } else if (typeof photoItem === 'string') { if (!fs.existsSync(photoItem)) { throw new Error(`Photo file not found: ${photoItem}`); } form.append('photos[]', createReadStream(photoItem)); // Pass file path as stream } else { throw new Error(`Invalid photo item: ${photoItem}. Must be a file path or URL string.`); } }); // Platform-specific parameters // LinkedIn if (options.visibility) form.append('visibility', options.visibility); if (options.target_linkedin_page_id) form.append('target_linkedin_page_id', options.target_linkedin_page_id); // Facebook if (options.facebook_page_id) form.append('facebook_page_id', options.facebook_page_id); // TikTok if (options.auto_add_music !== undefined) form.append('auto_add_music', options.auto_add_music.toString()); if (options.disable_comment !== undefined) form.append('disable_comment', options.disable_comment.toString()); if (options.branded_content !== undefined) form.append('branded_content', options.branded_content.toString()); if (options.disclose_commercial !== undefined) form.append('disclose_commercial', options.disclose_commercial.toString()); if (options.photo_cover_index !== undefined) form.append('photo_cover_index', options.photo_cover_index.toString()); if (options.description) form.append('description', options.description); // Instagram if (options.media_type) form.append('media_type', options.media_type); // Pinterest if (options.pinterest_board_id) form.append('pinterest_board_id', options.pinterest_board_id); if (options.pinterest_alt_text) form.append('pinterest_alt_text', options.pinterest_alt_text); if (options.pinterest_link) form.append('pinterest_link', options.pinterest_link); try { const response = await axios.post(`${this.baseUrl}/upload_photos`, form, { headers: { ...form.getHeaders(), 'Authorization': `Apikey ${this.apiKey}` }, maxContentLength: Infinity, maxBodyLength: Infinity }); if (response.status !== 200) { throw new Error(`API request failed with status ${response.status}`); } return response.data; } catch (error) { let errorMessage = `Photo upload failed: ${error.message}`; if (error.response && error.response.data && error.response.data.message) { errorMessage += ` - ${error.response.data.message}`; } else if (error.response && error.response.data && error.response.data.detail) { errorMessage += ` - ${error.response.data.detail}`; } throw new Error(errorMessage); } } /** * Upload text posts to social media platforms * @param {UploadTextOptions} options - Upload text options * @returns {Promise<Object>} API response */ async uploadText(options) { const form = new FormData(); // Common parameters form.append('user', options.user); form.append('title', options.title); // 'title' is used as the text content field for all supported platforms options.platforms.forEach(platform => { form.append('platform[]', platform); }); // Platform-specific parameters // LinkedIn if (options.target_linkedin_page_id && options.platforms.includes('linkedin')) { form.append('target_linkedin_page_id', options.target_linkedin_page_id); } // Facebook if (options.facebook_page_id && options.platforms.includes('facebook')) { form.append('facebook_page_id', options.facebook_page_id); } // X (Twitter) and Threads use the common 'title' parameter for their text content. try { const response = await axios.post(`${this.baseUrl}/upload_text`, form, { headers: { ...form.getHeaders(), 'Authorization': `Apikey ${this.apiKey}` }, }); if (response.status !== 200) { throw new Error(`API request failed with status ${response.status}`); } return response.data; } catch (error) { let errorMessage = `Text post upload failed: ${error.message}`; if (error.response && error.response.data) { if (error.response.data.message) { errorMessage += ` - ${error.response.data.message}`; } else if (error.response.data.detail) { errorMessage += ` - ${error.response.data.detail}`; } else { errorMessage += ` - ${JSON.stringify(error.response.data)}`; } } throw new Error(errorMessage); } } }