@memberjunction/actions-bizapps-social
Version:
Social Media Actions for MemberJunction - Twitter, LinkedIn, Facebook, Instagram, TikTok, YouTube, HootSuite, Buffer
322 lines (293 loc) • 12.2 kB
text/typescript
import { RegisterClass } from '@memberjunction/global';
import { YouTubeBaseAction } from '../youtube-base.action';
import { ActionParam, ActionResultSimple, RunActionParams } from '@memberjunction/actions-base';
import { BaseAction } from '@memberjunction/actions';
/**
* Action to update metadata for a YouTube video
*/
(BaseAction, 'YouTubeUpdateVideoMetadataAction')
export class YouTubeUpdateVideoMetadataAction extends YouTubeBaseAction {
/**
* Update video metadata on YouTube
*/
protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> {
const { Params, ContextUser } = params;
try {
// Initialize OAuth
const companyIntegrationId = this.getParamValue(Params, 'CompanyIntegrationID');
if (!companyIntegrationId) {
throw new Error('CompanyIntegrationID is required');
}
const initialized = await this.initializeOAuth(companyIntegrationId);
if (!initialized) {
throw new Error('Failed to initialize YouTube OAuth connection');
}
// Extract parameters
const videoId = this.getParamValue(Params, 'VideoID');
const title = this.getParamValue(Params, 'Title');
const description = this.getParamValue(Params, 'Description');
const tags = this.getParamValue(Params, 'Tags');
const categoryId = this.getParamValue(Params, 'CategoryID');
const privacyStatus = this.getParamValue(Params, 'PrivacyStatus');
const embeddable = this.getParamValue(Params, 'Embeddable');
const publicStatsViewable = this.getParamValue(Params, 'PublicStatsViewable');
const publishAt = this.getParamValue(Params, 'PublishAt');
const license = this.getParamValue(Params, 'License');
const recordingDate = this.getParamValue(Params, 'RecordingDate');
const thumbnailFile = this.getParamValue(Params, 'ThumbnailFile');
// Validate required parameters
if (!videoId) {
throw new Error('VideoID is required');
}
// First, get the current video details to preserve unchanged fields
const currentVideo = await this.makeYouTubeRequest<any>(
'/videos',
'GET',
undefined,
{
part: 'snippet,status',
id: videoId
},
ContextUser
);
if (!currentVideo.items || currentVideo.items.length === 0) {
throw new Error(`Video not found: ${videoId}`);
}
const video = currentVideo.items[0];
const currentSnippet = video.snippet;
const currentStatus = video.status;
// Build update request with only changed fields
const updateData: any = {
id: videoId,
snippet: {
title: title !== undefined ? title : currentSnippet.title,
description: description !== undefined ? description : currentSnippet.description,
tags: tags !== undefined ? tags : currentSnippet.tags,
categoryId: categoryId !== undefined ? categoryId : currentSnippet.categoryId
},
status: {
privacyStatus: privacyStatus !== undefined ? privacyStatus : currentStatus.privacyStatus,
embeddable: embeddable !== undefined ? embeddable : currentStatus.embeddable,
publicStatsViewable: publicStatsViewable !== undefined ? publicStatsViewable : currentStatus.publicStatsViewable,
selfDeclaredMadeForKids: currentStatus.selfDeclaredMadeForKids // Required field
}
};
// Add optional fields if provided
if (publishAt) {
updateData.status.publishAt = this.formatDate(publishAt);
}
if (license) {
updateData.status.license = license;
}
if (recordingDate) {
updateData.recordingDetails = {
recordingDate: this.formatDate(recordingDate)
};
}
// Update the video
const updateResponse = await this.makeYouTubeRequest<any>(
'/videos',
'PUT',
updateData,
{
part: 'snippet,status' + (recordingDate ? ',recordingDetails' : '')
},
ContextUser
);
// Update thumbnail if provided
if (thumbnailFile) {
try {
await this.uploadThumbnail(videoId, thumbnailFile);
} catch (error) {
// Log thumbnail upload failure but don't fail the entire operation
console.error('Failed to update thumbnail:', error);
}
}
// Get updated video details
const updatedVideo = await this.makeYouTubeRequest<any>(
'/videos',
'GET',
undefined,
{
part: 'snippet,status,statistics,contentDetails',
id: videoId
},
ContextUser
);
const finalVideo = updatedVideo.items[0];
// Prepare result
const result = {
videoId: finalVideo.id,
videoUrl: `https://www.youtube.com/watch?v=${finalVideo.id}`,
updatedFields: this.getUpdatedFields(currentSnippet, currentStatus, finalVideo.snippet, finalVideo.status),
title: finalVideo.snippet.title,
description: finalVideo.snippet.description,
tags: finalVideo.snippet.tags,
categoryId: finalVideo.snippet.categoryId,
privacyStatus: finalVideo.status.privacyStatus,
publishAt: finalVideo.status.publishAt,
embeddable: finalVideo.status.embeddable,
publicStatsViewable: finalVideo.status.publicStatsViewable,
thumbnails: finalVideo.snippet.thumbnails,
statistics: finalVideo.statistics,
quotaCost: this.getQuotaCost('videos.update')
};
// Update output parameters
const outputParams = [...Params];
const videoDetailsParam = outputParams.find(p => p.Name === 'VideoDetails');
if (videoDetailsParam) videoDetailsParam.Value = result;
const updatedFieldsParam = outputParams.find(p => p.Name === 'UpdatedFields');
if (updatedFieldsParam) updatedFieldsParam.Value = result.updatedFields;
return {
Success: true,
ResultCode: 'SUCCESS',
Message: `Video metadata updated successfully. ${result.updatedFields.length} fields changed.`,
Params: outputParams
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
return {
Success: false,
ResultCode: this.getErrorCode(errorMessage),
Message: `Failed to update video metadata: ${errorMessage}`,
Params
};
}
}
/**
* Upload thumbnail for a video
*/
private async uploadThumbnail(videoId: string, thumbnailFile: any): Promise<void> {
const thumbnailData = Buffer.isBuffer(thumbnailFile.data)
? thumbnailFile.data
: Buffer.from(thumbnailFile.data, 'base64');
// Validate thumbnail
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp'];
if (!allowedTypes.includes(thumbnailFile.mimeType)) {
throw new Error('Invalid thumbnail format. Supported formats: JPEG, PNG, GIF, BMP');
}
const maxSize = 2 * 1024 * 1024; // 2MB
if (thumbnailData.length > maxSize) {
throw new Error(`Thumbnail too large. Maximum size is 2MB`);
}
await this.makeYouTubeRequest(
'/thumbnails/set',
'POST',
thumbnailData,
{
videoId: videoId
}
);
}
/**
* Determine which fields were updated
*/
private getUpdatedFields(oldSnippet: any, oldStatus: any, newSnippet: any, newStatus: any): string[] {
const updated: string[] = [];
if (oldSnippet.title !== newSnippet.title) updated.push('title');
if (oldSnippet.description !== newSnippet.description) updated.push('description');
if (JSON.stringify(oldSnippet.tags) !== JSON.stringify(newSnippet.tags)) updated.push('tags');
if (oldSnippet.categoryId !== newSnippet.categoryId) updated.push('categoryId');
if (oldStatus.privacyStatus !== newStatus.privacyStatus) updated.push('privacyStatus');
if (oldStatus.embeddable !== newStatus.embeddable) updated.push('embeddable');
if (oldStatus.publicStatsViewable !== newStatus.publicStatsViewable) updated.push('publicStatsViewable');
if (oldStatus.publishAt !== newStatus.publishAt) updated.push('publishAt');
return updated;
}
/**
* Get error code from error message
*/
private getErrorCode(message: string): string {
if (message.includes('quota')) return 'QUOTA_EXCEEDED';
if (message.includes('401') || message.includes('unauthorized')) return 'INVALID_TOKEN';
if (message.includes('403') || message.includes('forbidden')) return 'INSUFFICIENT_PERMISSIONS';
if (message.includes('404')) return 'NOT_FOUND';
if (message.includes('rate limit')) return 'RATE_LIMIT';
return 'ERROR';
}
/**
* Define the parameters this action expects
*/
public get Params(): ActionParam[] {
const baseParams = this.commonSocialParams;
const specificParams: ActionParam[] = [
{
Name: 'VideoID',
Type: 'Input',
Value: null
},
{
Name: 'Title',
Type: 'Input',
Value: null
},
{
Name: 'Description',
Type: 'Input',
Value: null
},
{
Name: 'Tags',
Type: 'Input',
Value: null
},
{
Name: 'CategoryID',
Type: 'Input',
Value: null
},
{
Name: 'PrivacyStatus',
Type: 'Input',
Value: null
},
{
Name: 'Embeddable',
Type: 'Input',
Value: null
},
{
Name: 'PublicStatsViewable',
Type: 'Input',
Value: null
},
{
Name: 'PublishAt',
Type: 'Input',
Value: null
},
{
Name: 'License',
Type: 'Input',
Value: null
},
{
Name: 'RecordingDate',
Type: 'Input',
Value: null
},
{
Name: 'ThumbnailFile',
Type: 'Input',
Value: null
},
{
Name: 'VideoDetails',
Type: 'Output',
Value: null
},
{
Name: 'UpdatedFields',
Type: 'Output',
Value: null
}
];
return [...baseParams, ...specificParams];
}
/**
* Metadata about this action
*/
public get Description(): string {
return 'Updates metadata for a YouTube video including title, description, tags, privacy settings, and thumbnail';
}
}