@memberjunction/actions-bizapps-social
Version:
Social Media Actions for MemberJunction - Twitter, LinkedIn, Facebook, Instagram, TikTok, YouTube, HootSuite, Buffer
503 lines (445 loc) • 18.6 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 a YouTube channel
*/
(BaseAction, 'YouTubeGetChannelAnalyticsAction')
export class YouTubeGetChannelAnalyticsAction extends YouTubeBaseAction {
/**
* Get analytics for a YouTube channel
*/
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 channelId = this.getParamValue(Params, 'ChannelID') || this.getCustomAttribute(1);
const dateRange = this.getParamValue(Params, 'DateRange') || 'last30Days';
const startDate = this.getParamValue(Params, 'StartDate');
const endDate = this.getParamValue(Params, 'EndDate');
const includeVideos = this.getParamValue(Params, 'IncludeVideos') ?? true;
const videoLimit = this.getParamValue(Params, 'VideoLimit') || 50;
// If no channel ID provided, get the authenticated user's channel
let actualChannelId = channelId;
if (!actualChannelId) {
const channelResponse = await this.makeYouTubeRequest<any>(
'/channels',
'GET',
undefined,
{
part: 'id,snippet,statistics,contentDetails,brandingSettings',
mine: true
},
ContextUser
);
if (!channelResponse.items || channelResponse.items.length === 0) {
throw new Error('No channel found for authenticated user');
}
actualChannelId = channelResponse.items[0].id;
} else {
// Get channel details for specified channel
const channelResponse = await this.makeYouTubeRequest<any>(
'/channels',
'GET',
undefined,
{
part: 'snippet,statistics,contentDetails,brandingSettings',
id: actualChannelId
},
ContextUser
);
if (!channelResponse.items || channelResponse.items.length === 0) {
throw new Error(`Channel not found: ${actualChannelId}`);
}
}
// Get channel details
const channelData = await this.getChannelDetails(actualChannelId, ContextUser);
const channel = channelData.items[0];
// Calculate date range
const { start, end } = this.calculateDateRange(dateRange, startDate, endDate);
// Get videos for the date range if requested
let videos: any[] = [];
let videoAnalytics: any = {};
if (includeVideos) {
videos = await this.getChannelVideosForDateRange(actualChannelId, start, end, videoLimit, ContextUser);
if (videos.length > 0) {
videoAnalytics = await this.aggregateVideoAnalytics(videos);
}
}
// Prepare channel analytics
const channelAnalytics = {
channelId: channel.id,
channelTitle: channel.snippet.title,
channelDescription: channel.snippet.description,
channelUrl: `https://www.youtube.com/channel/${channel.id}`,
customUrl: channel.snippet.customUrl,
country: channel.snippet.country,
publishedAt: channel.snippet.publishedAt,
thumbnails: channel.snippet.thumbnails,
// Overall statistics
statistics: {
viewCount: parseInt(channel.statistics.viewCount || '0'),
subscriberCount: parseInt(channel.statistics.subscriberCount || '0'),
videoCount: parseInt(channel.statistics.videoCount || '0'),
// Note: hiddenSubscriberCount indicates if subscriber count is public
subscribersHidden: channel.statistics.hiddenSubscriberCount
},
// Date range specific analytics
dateRangeAnalytics: {
dateRange: {
start: start.toISOString(),
end: end.toISOString(),
days: Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))
},
videosPublished: videos.length,
totalViews: videoAnalytics.totalViews || 0,
totalLikes: videoAnalytics.totalLikes || 0,
totalComments: videoAnalytics.totalComments || 0,
averageViewsPerVideo: videoAnalytics.averageViews || 0,
topPerformingVideos: videoAnalytics.topVideos || []
},
// Growth metrics (if we had historical data)
growth: {
note: 'Growth metrics require YouTube Analytics API access',
subscriberGrowth: 'N/A',
viewGrowth: 'N/A'
},
// Content details
contentDetails: {
relatedPlaylists: channel.contentDetails.relatedPlaylists,
uploadPlaylistId: channel.contentDetails.relatedPlaylists.uploads
},
// Branding
branding: {
keywords: channel.brandingSettings?.channel?.keywords,
unsubscribedTrailer: channel.brandingSettings?.channel?.unsubscribedTrailer,
featuredChannelsUrls: channel.brandingSettings?.channel?.featuredChannelsUrls
},
quotaCost: this.getQuotaCost('channels.list') +
(videos.length > 0 ? this.getQuotaCost('search.list') + this.getQuotaCost('videos.list') : 0)
};
// Prepare summary
const summary = {
channelHealth: this.calculateChannelHealth(channel, videoAnalytics),
recommendations: this.generateRecommendations(channel, videoAnalytics),
performanceIndicators: {
engagementRate: this.calculateEngagementRate(channel.statistics, videoAnalytics),
averageViewsPerSubscriber: this.calculateViewsPerSubscriber(channel.statistics),
uploadFrequency: this.calculateUploadFrequency(videos, start, end)
}
};
// Update output parameters
const outputParams = [...Params];
const analyticsParam = outputParams.find(p => p.Name === 'Analytics');
if (analyticsParam) analyticsParam.Value = channelAnalytics;
const summaryParam = outputParams.find(p => p.Name === 'Summary');
if (summaryParam) summaryParam.Value = summary;
const videosParam = outputParams.find(p => p.Name === 'Videos');
if (videosParam) videosParam.Value = videos;
return {
Success: true,
ResultCode: 'SUCCESS',
Message: `Retrieved analytics for channel: ${channel.snippet.title}`,
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 channel analytics: ${errorMessage}`,
Params
};
}
}
/**
* Get channel details
*/
private async getChannelDetails(channelId: string, contextUser?: any): Promise<any> {
return await this.makeYouTubeRequest(
'/channels',
'GET',
undefined,
{
part: 'snippet,statistics,contentDetails,brandingSettings',
id: channelId
},
contextUser
);
}
/**
* Get videos for a date range
*/
private async getChannelVideosForDateRange(
channelId: string,
startDate: Date,
endDate: Date,
limit: number,
contextUser?: any
): Promise<any[]> {
const searchResponse = await this.makeYouTubeRequest<any>(
'/search',
'GET',
undefined,
{
part: 'id',
channelId: channelId,
type: 'video',
publishedAfter: startDate.toISOString(),
publishedBefore: endDate.toISOString(),
maxResults: Math.min(limit, 50),
order: 'date'
},
contextUser
);
if (!searchResponse.items || searchResponse.items.length === 0) {
return [];
}
const videoIds = searchResponse.items.map((item: any) => item.id.videoId).join(',');
const videosResponse = await this.makeYouTubeRequest<any>(
'/videos',
'GET',
undefined,
{
part: 'snippet,statistics,contentDetails',
id: videoIds
},
contextUser
);
return videosResponse.items || [];
}
/**
* Aggregate video analytics
*/
private aggregateVideoAnalytics(videos: any[]): any {
let totalViews = 0;
let totalLikes = 0;
let totalComments = 0;
let totalDislikes = 0;
for (const video of videos) {
const stats = video.statistics || {};
totalViews += parseInt(stats.viewCount || '0');
totalLikes += parseInt(stats.likeCount || '0');
totalComments += parseInt(stats.commentCount || '0');
totalDislikes += parseInt(stats.dislikeCount || '0');
}
const topVideos = [...videos]
.sort((a, b) => parseInt(b.statistics?.viewCount || '0') - parseInt(a.statistics?.viewCount || '0'))
.slice(0, 5)
.map(v => ({
id: v.id,
title: v.snippet.title,
views: parseInt(v.statistics?.viewCount || '0'),
likes: parseInt(v.statistics?.likeCount || '0'),
comments: parseInt(v.statistics?.commentCount || '0'),
publishedAt: v.snippet.publishedAt,
url: `https://www.youtube.com/watch?v=${v.id}`
}));
return {
totalViews,
totalLikes,
totalComments,
totalDislikes,
averageViews: videos.length > 0 ? Math.round(totalViews / videos.length) : 0,
averageLikes: videos.length > 0 ? Math.round(totalLikes / videos.length) : 0,
topVideos
};
}
/**
* Calculate date range
*/
private calculateDateRange(
dateRange: string,
startDate?: string,
endDate?: string
): { start: Date; end: Date } {
const end = endDate ? new Date(endDate) : new Date();
let start: Date;
if (startDate) {
start = new Date(startDate);
} else {
switch (dateRange) {
case 'last7Days':
start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000);
break;
case 'last30Days':
start = new Date(end.getTime() - 30 * 24 * 60 * 60 * 1000);
break;
case 'last90Days':
start = new Date(end.getTime() - 90 * 24 * 60 * 60 * 1000);
break;
case 'lastYear':
start = new Date(end.getTime() - 365 * 24 * 60 * 60 * 1000);
break;
default:
start = new Date(end.getTime() - 30 * 24 * 60 * 60 * 1000);
}
}
return { start, end };
}
/**
* Calculate channel health score
*/
private calculateChannelHealth(channel: any, videoAnalytics: any): any {
const stats = channel.statistics;
const subscribers = parseInt(stats.subscriberCount || '0');
const views = parseInt(stats.viewCount || '0');
const videos = parseInt(stats.videoCount || '0');
// Simple health score based on engagement
let score = 0;
let factors: string[] = [];
// Subscriber to view ratio
if (subscribers > 0 && views > subscribers * 100) {
score += 25;
factors.push('Good view-to-subscriber ratio');
}
// Video count
if (videos > 50) {
score += 25;
factors.push('Active content creation');
}
// Recent engagement
if (videoAnalytics.averageViews > subscribers * 0.1) {
score += 25;
factors.push('Strong recent engagement');
}
// Upload consistency
if (videoAnalytics.topVideos?.length >= 3) {
score += 25;
factors.push('Consistent uploads');
}
return {
score: Math.min(score, 100),
rating: score >= 75 ? 'Excellent' : score >= 50 ? 'Good' : score >= 25 ? 'Fair' : 'Needs Improvement',
factors
};
}
/**
* Generate recommendations
*/
private generateRecommendations(channel: any, videoAnalytics: any): string[] {
const recommendations: string[] = [];
const stats = channel.statistics;
const subscribers = parseInt(stats.subscriberCount || '0');
if (videoAnalytics.averageViews < subscribers * 0.05) {
recommendations.push('Consider improving video titles and thumbnails to increase click-through rate');
}
if (videoAnalytics.topVideos?.length < 3) {
recommendations.push('Increase upload frequency to maintain audience engagement');
}
if (!channel.brandingSettings?.channel?.keywords) {
recommendations.push('Add channel keywords to improve discoverability');
}
if (!channel.snippet.customUrl) {
recommendations.push('Claim a custom URL for easier channel sharing');
}
return recommendations;
}
/**
* Calculate engagement rate
*/
private calculateEngagementRate(stats: any, videoAnalytics: any): number {
const views = videoAnalytics.totalViews || parseInt(stats.viewCount || '0');
const engagements = (videoAnalytics.totalLikes || 0) + (videoAnalytics.totalComments || 0);
return views > 0 ? Math.round((engagements / views) * 10000) / 100 : 0;
}
/**
* Calculate views per subscriber
*/
private calculateViewsPerSubscriber(stats: any): number {
const views = parseInt(stats.viewCount || '0');
const subscribers = parseInt(stats.subscriberCount || '0');
return subscribers > 0 ? Math.round((views / subscribers) * 100) / 100 : 0;
}
/**
* Calculate upload frequency
*/
private calculateUploadFrequency(videos: any[], start: Date, end: Date): string {
const days = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
const videosPerDay = videos.length / days;
if (videosPerDay >= 1) return `${Math.round(videosPerDay * 10) / 10} videos/day`;
if (videosPerDay >= 1/7) return `${Math.round(videosPerDay * 7 * 10) / 10} videos/week`;
return `${Math.round(videosPerDay * 30 * 10) / 10} videos/month`;
}
/**
* 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: 'ChannelID',
Type: 'Input',
Value: null
},
{
Name: 'DateRange',
Type: 'Input',
Value: 'last30Days'
},
{
Name: 'StartDate',
Type: 'Input',
Value: null
},
{
Name: 'EndDate',
Type: 'Input',
Value: null
},
{
Name: 'IncludeVideos',
Type: 'Input',
Value: true
},
{
Name: 'VideoLimit',
Type: 'Input',
Value: 50
},
{
Name: 'Analytics',
Type: 'Output',
Value: null
},
{
Name: 'Summary',
Type: 'Output',
Value: null
},
{
Name: 'Videos',
Type: 'Output',
Value: null
}
];
return [...baseParams, ...specificParams];
}
/**
* Metadata about this action
*/
public get Description(): string {
return 'Gets comprehensive analytics for a YouTube channel including statistics, growth metrics, and performance indicators';
}
}