@memberjunction/actions-bizapps-social
Version:
Social Media Actions for MemberJunction - Twitter, LinkedIn, Facebook, Instagram, TikTok, YouTube, HootSuite, Buffer
320 lines • 12.5 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.YouTubeBaseAction = void 0;
const global_1 = require("@memberjunction/global");
const base_social_action_1 = require("../../base/base-social.action");
const axios_1 = __importDefault(require("axios"));
const actions_1 = require("@memberjunction/actions");
/**
* Base class for all YouTube actions.
* Handles YouTube Data API v3 authentication and common functionality.
*/
let YouTubeBaseAction = class YouTubeBaseAction extends base_social_action_1.BaseSocialMediaAction {
get platformName() {
return 'YouTube';
}
get apiBaseUrl() {
return 'https://www.googleapis.com/youtube/v3';
}
/**
* Axios instance for API requests
*/
axiosInstance;
/**
* Initialize the axios instance with base configuration
*/
initializeAxios() {
this.axiosInstance = axios_1.default.create({
baseURL: this.apiBaseUrl,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
}
/**
* YouTube-specific OAuth token refresh
*/
async refreshAccessToken() {
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available for YouTube');
}
try {
const response = await axios_1.default.post('https://oauth2.googleapis.com/token', {
client_id: this.getCustomAttribute(2), // Store client ID in CustomAttribute2
client_secret: this.getCustomAttribute(3), // Store client secret in CustomAttribute3
refresh_token: refreshToken,
grant_type: 'refresh_token'
});
const { access_token, expires_in } = response.data;
await this.updateStoredTokens(access_token, undefined, expires_in);
}
catch (error) {
throw new Error(`Failed to refresh YouTube access token: ${error.message}`);
}
}
/**
* Make authenticated request to YouTube API
*/
async makeYouTubeRequest(endpoint, method = 'GET', data, params, contextUser) {
if (!this.axiosInstance) {
this.initializeAxios();
}
return this.makeAuthenticatedRequest(async (token) => {
try {
const response = await this.axiosInstance.request({
url: endpoint,
method,
data,
params,
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.data;
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
this.handleYouTubeApiError(error);
}
throw error;
}
});
}
/**
* Handle YouTube API errors
*/
handleYouTubeApiError(error) {
const response = error.response;
if (!response) {
throw new Error('Network error occurred');
}
const errorData = response.data;
const errorMessage = errorData?.error?.message || response.statusText;
const errorCode = errorData?.error?.code || response.status;
// Check for quota exceeded
if (errorCode === 403 && errorMessage.includes('quota')) {
throw new Error(`YouTube API quota exceeded. ${errorMessage}`);
}
// Check for rate limiting
if (errorCode === 429) {
const retryAfter = response.headers['retry-after'];
throw new Error(`Rate limit exceeded. Retry after ${retryAfter || '60'} seconds`);
}
throw new Error(`YouTube API error (${errorCode}): ${errorMessage}`);
}
/**
* Upload video to YouTube
*/
async uploadVideo(videoFile, metadata) {
// YouTube requires a resumable upload for videos
const uploadUrl = await this.initiateResumableUpload(metadata);
const videoId = await this.performResumableUpload(uploadUrl, videoFile);
return videoId;
}
/**
* Initiate resumable upload session
*/
async initiateResumableUpload(metadata) {
const response = await this.makeYouTubeRequest('/videos', 'POST', {
snippet: {
title: metadata.title,
description: metadata.description || '',
tags: metadata.tags || [],
categoryId: metadata.categoryId || '22' // Default to People & Blogs
},
status: {
privacyStatus: metadata.privacyStatus || 'private'
}
}, {
uploadType: 'resumable',
part: 'snippet,status'
});
return response.headers.location;
}
/**
* Perform the actual video upload
*/
async performResumableUpload(uploadUrl, videoFile) {
const videoData = Buffer.isBuffer(videoFile.data)
? videoFile.data
: Buffer.from(videoFile.data, 'base64');
const response = await axios_1.default.put(uploadUrl, videoData, {
headers: {
'Content-Type': videoFile.mimeType,
'Content-Length': videoData.length.toString(),
'Authorization': `Bearer ${this.getAccessToken()}`
}
});
return response.data.id;
}
/**
* Upload single media file (thumbnail)
*/
async uploadSingleMedia(file) {
// For YouTube, this would typically be used for thumbnails
// The actual implementation would upload to YouTube's thumbnail endpoint
throw new Error('Direct media upload not supported. Use uploadVideo for videos or setThumbnail for thumbnails.');
}
/**
* Convert YouTube video to standard social post format
*/
normalizePost(youtubeVideo) {
return {
id: youtubeVideo.id,
platform: 'YouTube',
profileId: youtubeVideo.snippet.channelId,
content: youtubeVideo.snippet.description || '',
mediaUrls: [`https://www.youtube.com/watch?v=${youtubeVideo.id}`],
publishedAt: new Date(youtubeVideo.snippet.publishedAt),
scheduledFor: youtubeVideo.status.publishAt ? new Date(youtubeVideo.status.publishAt) : undefined,
analytics: this.extractVideoAnalytics(youtubeVideo),
platformSpecificData: {
title: youtubeVideo.snippet.title,
tags: youtubeVideo.snippet.tags || [],
categoryId: youtubeVideo.snippet.categoryId,
duration: youtubeVideo.contentDetails?.duration,
definition: youtubeVideo.contentDetails?.definition,
privacyStatus: youtubeVideo.status.privacyStatus,
embeddable: youtubeVideo.status.embeddable,
thumbnails: youtubeVideo.snippet.thumbnails
}
};
}
/**
* Extract analytics from video statistics
*/
extractVideoAnalytics(video) {
if (!video.statistics) {
return undefined;
}
return this.normalizeAnalytics({
impressions: parseInt(video.statistics.viewCount || '0'),
engagements: parseInt(video.statistics.likeCount || '0') +
parseInt(video.statistics.commentCount || '0'),
clicks: 0, // YouTube doesn't provide click data
shares: 0, // YouTube doesn't provide share count
comments: parseInt(video.statistics.commentCount || '0'),
likes: parseInt(video.statistics.likeCount || '0'),
reach: parseInt(video.statistics.viewCount || '0'),
saves: parseInt(video.statistics.favoriteCount || '0'),
videoViews: parseInt(video.statistics.viewCount || '0'),
dislikes: parseInt(video.statistics.dislikeCount || '0')
});
}
/**
* Search for videos
*/
async searchPosts(params) {
const searchParams = {
part: 'snippet',
type: 'video',
maxResults: params.limit || 50,
order: params.sortBy || 'relevance'
};
// Add search query
if (params.query) {
searchParams.q = params.query;
}
// Add channel filter if searching within a specific channel
if (params.channelId || this.getCustomAttribute(1)) {
searchParams.channelId = params.channelId || this.getCustomAttribute(1);
}
// Add date filters
if (params.startDate) {
searchParams.publishedAfter = this.formatDate(params.startDate);
}
if (params.endDate) {
searchParams.publishedBefore = this.formatDate(params.endDate);
}
// Add pagination
if (params.pageToken) {
searchParams.pageToken = params.pageToken;
}
const response = await this.makeYouTubeRequest('/search', 'GET', undefined, searchParams);
// Get full video details for search results
const videoIds = response.items.map((item) => item.id.videoId).join(',');
const videosResponse = await this.makeYouTubeRequest('/videos', 'GET', undefined, {
part: 'snippet,statistics,status,contentDetails',
id: videoIds
});
return videosResponse.items.map((video) => this.normalizePost(video));
}
/**
* Get quota cost for an operation
*/
getQuotaCost(operation) {
const quotaCosts = {
'videos.list': 1,
'videos.insert': 1600,
'videos.update': 50,
'videos.delete': 50,
'search.list': 100,
'channels.list': 1,
'playlists.list': 1,
'playlists.insert': 50,
'playlistItems.insert': 50,
'comments.list': 1,
'commentThreads.list': 1
};
return quotaCosts[operation] || 1;
}
/**
* Parse ISO 8601 duration to seconds
*/
parseDuration(isoDuration) {
const matches = isoDuration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
if (!matches)
return 0;
const hours = parseInt(matches[1] || '0');
const minutes = parseInt(matches[2] || '0');
const seconds = parseInt(matches[3] || '0');
return hours * 3600 + minutes * 60 + seconds;
}
/**
* Format bytes to human readable size
*/
formatBytes(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0)
return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
/**
* Validate video file
*/
validateVideoFile(file) {
const allowedTypes = [
'video/mp4',
'video/x-msvideo', // AVI
'video/quicktime', // MOV
'video/x-ms-wmv', // WMV
'video/x-flv', // FLV
'video/webm'
];
if (!allowedTypes.includes(file.mimeType)) {
throw new Error(`Unsupported video format: ${file.mimeType}`);
}
// YouTube max file size is 128GB or 12 hours
const maxSize = 128 * 1024 * 1024 * 1024; // 128GB
if (file.size > maxSize) {
throw new Error(`Video file too large. Maximum size is ${this.formatBytes(maxSize)}`);
}
}
};
exports.YouTubeBaseAction = YouTubeBaseAction;
exports.YouTubeBaseAction = YouTubeBaseAction = __decorate([
(0, global_1.RegisterClass)(actions_1.BaseAction, 'YouTubeBaseAction')
], YouTubeBaseAction);
//# sourceMappingURL=youtube-base.action.js.map