@memberjunction/actions-bizapps-social
Version:
Social Media Actions for MemberJunction - Twitter, LinkedIn, Facebook, Instagram, TikTok, YouTube, HootSuite, Buffer
299 lines (265 loc) • 11.3 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 get analytics for YouTube videos
*/
(BaseAction, 'YouTubeGetVideoAnalyticsAction')
export class YouTubeGetVideoAnalyticsAction extends YouTubeBaseAction {
/**
* Get analytics for YouTube videos
*/
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 videoIds = this.getParamValue(Params, 'VideoIDs');
const metricsRequested = this.getParamValue(Params, 'Metrics') || ['views', 'likes', 'comments', 'shares'];
const startDate = this.getParamValue(Params, 'StartDate');
const endDate = this.getParamValue(Params, 'EndDate');
const dimensions = this.getParamValue(Params, 'Dimensions');
// Validate parameters
if (!videoIds || (Array.isArray(videoIds) && videoIds.length === 0)) {
throw new Error('VideoIDs parameter is required');
}
// Convert single video ID to array
const videoIdArray = Array.isArray(videoIds) ? videoIds : [videoIds];
// YouTube Data API provides basic statistics
// For advanced analytics, YouTube Analytics API would be needed
const videoDetails = await this.getVideoStatistics(videoIdArray, ContextUser);
// Process analytics for each video
const analyticsData = videoDetails.map((video: any) => {
const stats = video.statistics || {};
const snippet = video.snippet || {};
return {
videoId: video.id,
title: snippet.title,
publishedAt: snippet.publishedAt,
metrics: {
views: parseInt(stats.viewCount || '0'),
likes: parseInt(stats.likeCount || '0'),
dislikes: parseInt(stats.dislikeCount || '0'),
comments: parseInt(stats.commentCount || '0'),
favorites: parseInt(stats.favoriteCount || '0'),
// Engagement rate calculation
engagementRate: this.calculateEngagementRate(stats),
// Like/dislike ratio
likeRatio: this.calculateLikeRatio(stats)
},
// Additional metadata
duration: video.contentDetails?.duration,
privacyStatus: video.status?.privacyStatus,
categoryId: snippet.categoryId,
tags: snippet.tags || [],
// Thumbnail for reference
thumbnail: snippet.thumbnails?.high?.url || snippet.thumbnails?.default?.url
};
});
// Calculate aggregate metrics
const aggregateMetrics = this.calculateAggregateMetrics(analyticsData);
// If YouTube Analytics API was available, we could get:
// - Watch time
// - Average view duration
// - Impressions
// - Click-through rate
// - Revenue data
// - Geographic data
// - Traffic sources
// - Device types
// - Audience retention
// Create summary
const summary = {
totalVideos: analyticsData.length,
dateRange: {
start: startDate || 'N/A',
end: endDate || 'N/A'
},
aggregateMetrics: aggregateMetrics,
topPerformers: this.getTopPerformers(analyticsData),
quotaCost: this.getQuotaCost('videos.list') * Math.ceil(videoIdArray.length / 50)
};
// Update output parameters
const outputParams = [...Params];
const analyticsParam = outputParams.find(p => p.Name === 'Analytics');
if (analyticsParam) analyticsParam.Value = analyticsData;
const summaryParam = outputParams.find(p => p.Name === 'Summary');
if (summaryParam) summaryParam.Value = summary;
return {
Success: true,
ResultCode: 'SUCCESS',
Message: `Retrieved analytics for ${analyticsData.length} videos`,
Params: outputParams
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
return {
Success: false,
ResultCode: this.getErrorCode(errorMessage),
Message: `Failed to get video analytics: ${errorMessage}`,
Params
};
}
}
/**
* Get video statistics from YouTube Data API
*/
private async getVideoStatistics(videoIds: string[], contextUser?: any): Promise<any[]> {
const results: any[] = [];
// YouTube allows up to 50 video IDs per request
const chunks = this.chunkArray(videoIds, 50);
for (const chunk of chunks) {
const response = await this.makeYouTubeRequest<any>(
'/videos',
'GET',
undefined,
{
part: 'snippet,statistics,contentDetails,status',
id: chunk.join(',')
},
contextUser
);
if (response.items) {
results.push(...response.items);
}
}
return results;
}
/**
* Calculate engagement rate
*/
private calculateEngagementRate(stats: any): number {
const views = parseInt(stats.viewCount || '0');
if (views === 0) return 0;
const engagements = parseInt(stats.likeCount || '0') +
parseInt(stats.dislikeCount || '0') +
parseInt(stats.commentCount || '0');
return Math.round((engagements / views) * 10000) / 100; // Percentage with 2 decimals
}
/**
* Calculate like ratio
*/
private calculateLikeRatio(stats: any): number {
const likes = parseInt(stats.likeCount || '0');
const dislikes = parseInt(stats.dislikeCount || '0');
const total = likes + dislikes;
if (total === 0) return 0;
return Math.round((likes / total) * 10000) / 100; // Percentage with 2 decimals
}
/**
* Calculate aggregate metrics across all videos
*/
private calculateAggregateMetrics(analyticsData: any[]): any {
const totals = analyticsData.reduce((acc, video) => {
acc.views += video.metrics.views;
acc.likes += video.metrics.likes;
acc.dislikes += video.metrics.dislikes;
acc.comments += video.metrics.comments;
acc.favorites += video.metrics.favorites;
return acc;
}, { views: 0, likes: 0, dislikes: 0, comments: 0, favorites: 0 });
const avgEngagement = analyticsData.reduce((sum, v) => sum + v.metrics.engagementRate, 0) / analyticsData.length;
const avgLikeRatio = analyticsData.reduce((sum, v) => sum + v.metrics.likeRatio, 0) / analyticsData.length;
return {
total: totals,
average: {
views: Math.round(totals.views / analyticsData.length),
likes: Math.round(totals.likes / analyticsData.length),
dislikes: Math.round(totals.dislikes / analyticsData.length),
comments: Math.round(totals.comments / analyticsData.length),
engagementRate: Math.round(avgEngagement * 100) / 100,
likeRatio: Math.round(avgLikeRatio * 100) / 100
}
};
}
/**
* Get top performing videos
*/
private getTopPerformers(analyticsData: any[]): any {
return {
byViews: [...analyticsData].sort((a, b) => b.metrics.views - a.metrics.views).slice(0, 5),
byEngagement: [...analyticsData].sort((a, b) => b.metrics.engagementRate - a.metrics.engagementRate).slice(0, 5),
byLikes: [...analyticsData].sort((a, b) => b.metrics.likes - a.metrics.likes).slice(0, 5)
};
}
/**
* Chunk array into smaller arrays
*/
private chunkArray<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
/**
* 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: 'VideoIDs',
Type: 'Input',
Value: null
},
{
Name: 'Metrics',
Type: 'Input',
Value: ['views', 'likes', 'comments', 'shares']
},
{
Name: 'StartDate',
Type: 'Input',
Value: null
},
{
Name: 'EndDate',
Type: 'Input',
Value: null
},
{
Name: 'Dimensions',
Type: 'Input',
Value: null
},
{
Name: 'Analytics',
Type: 'Output',
Value: null
},
{
Name: 'Summary',
Type: 'Output',
Value: null
}
];
return [...baseParams, ...specificParams];
}
/**
* Metadata about this action
*/
public get Description(): string {
return 'Gets analytics data for YouTube videos including views, engagement, and performance metrics';
}
}