UNPKG

@memberjunction/actions-bizapps-social

Version:

Social Media Actions for MemberJunction - Twitter, LinkedIn, Facebook, Instagram, TikTok, YouTube, HootSuite, Buffer

531 lines (479 loc) 18.7 kB
import { RegisterClass } from '@memberjunction/global'; import { YouTubeBaseAction } from '../youtube-base.action'; import { ActionParam, ActionResultSimple, RunActionParams } from '@memberjunction/actions-base'; import { SocialPost, SearchParams } from '../../../base/base-social.action'; import { BaseAction } from '@memberjunction/actions'; /** * Action to search for YouTube videos including historical content */ @RegisterClass(BaseAction, 'YouTubeSearchVideosAction') export class YouTubeSearchVideosAction extends YouTubeBaseAction { /** * Search 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 query = this.getParamValue(Params, 'Query'); const channelId = this.getParamValue(Params, 'ChannelID'); const startDate = this.getParamValue(Params, 'StartDate'); const endDate = this.getParamValue(Params, 'EndDate'); const videoDuration = this.getParamValue(Params, 'VideoDuration'); const videoType = this.getParamValue(Params, 'VideoType'); const orderBy = this.getParamValue(Params, 'OrderBy') || 'relevance'; const maxResults = this.getParamValue(Params, 'MaxResults') || 50; const location = this.getParamValue(Params, 'Location'); const locationRadius = this.getParamValue(Params, 'LocationRadius'); const language = this.getParamValue(Params, 'Language'); const safeSearch = this.getParamValue(Params, 'SafeSearch') || 'moderate'; const videoCaption = this.getParamValue(Params, 'VideoCaption'); const videoDefinition = this.getParamValue(Params, 'VideoDefinition'); const videoDimension = this.getParamValue(Params, 'VideoDimension'); const videoLicense = this.getParamValue(Params, 'VideoLicense'); const pageToken = this.getParamValue(Params, 'PageToken'); const includeHistorical = this.getParamValue(Params, 'IncludeHistorical') ?? true; // Build search parameters const searchParams: SearchParams = { query: query, startDate: startDate ? new Date(startDate) : undefined, endDate: endDate ? new Date(endDate) : undefined, limit: maxResults }; // If searching within own channel, use channel ID from integration const actualChannelId = channelId || (includeHistorical ? this.getCustomAttribute(1) : null); // Perform search let allVideos: SocialPost[] = []; let nextPageToken = pageToken; let totalResults = 0; let pagesSearched = 0; const maxPages = includeHistorical ? 10 : 1; // Search up to 10 pages for historical data do { const searchResponse = await this.searchYouTubeVideos({ ...searchParams, channelId: actualChannelId, videoDuration, videoType, orderBy, location, locationRadius, language, safeSearch, videoCaption, videoDefinition, videoDimension, videoLicense, pageToken: nextPageToken }, ContextUser); if (searchResponse.videos && searchResponse.videos.length > 0) { allVideos.push(...searchResponse.videos); totalResults = searchResponse.totalResults || allVideos.length; } nextPageToken = searchResponse.nextPageToken; pagesSearched++; // Continue searching if we haven't reached the limit and there are more pages } while ( includeHistorical && nextPageToken && allVideos.length < maxResults && pagesSearched < maxPages ); // Trim to max results if (allVideos.length > maxResults) { allVideos = allVideos.slice(0, maxResults); } // Group videos by various criteria for analytics const analytics = this.analyzeSearchResults(allVideos); // Create summary const summary = { totalResults: totalResults, videosReturned: allVideos.length, pagesSearched: pagesSearched, searchCriteria: { query: query, channelId: actualChannelId, dateRange: { start: startDate || 'Any', end: endDate || 'Any' }, filters: { duration: videoDuration || 'Any', type: videoType || 'Any', definition: videoDefinition || 'Any', caption: videoCaption || 'Any' } }, analytics: analytics, nextPageToken: nextPageToken, hasMore: !!nextPageToken, quotaCost: this.getQuotaCost('search.list') * pagesSearched + this.getQuotaCost('videos.list') * Math.ceil(allVideos.length / 50) }; // Update output parameters const outputParams = [...Params]; const videosParam = outputParams.find(p => p.Name === 'Videos'); if (videosParam) videosParam.Value = allVideos; const summaryParam = outputParams.find(p => p.Name === 'Summary'); if (summaryParam) summaryParam.Value = summary; const nextPageTokenParam = outputParams.find(p => p.Name === 'NextPageToken'); if (nextPageTokenParam) nextPageTokenParam.Value = nextPageToken; return { Success: true, ResultCode: 'SUCCESS', Message: `Found ${allVideos.length} videos${totalResults > allVideos.length ? ` out of ${totalResults} total results` : ''}`, Params: outputParams }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { Success: false, ResultCode: this.getErrorCode(errorMessage), Message: `Failed to search videos: ${errorMessage}`, Params }; } } /** * Search YouTube videos with detailed parameters */ private async searchYouTubeVideos(params: any, contextUser?: any): Promise<any> { const searchParams: any = { part: 'id,snippet', type: 'video', maxResults: Math.min(params.limit || 50, 50), order: params.orderBy || 'relevance' }; // Add search query if (params.query) { searchParams.q = params.query; } // Add channel filter if (params.channelId) { searchParams.channelId = params.channelId; } // Add date filters for historical search if (params.startDate) { searchParams.publishedAfter = this.formatDate(params.startDate); } if (params.endDate) { searchParams.publishedBefore = this.formatDate(params.endDate); } // Add video duration filter if (params.videoDuration) { searchParams.videoDuration = params.videoDuration; // short, medium, long } // Add video type filter if (params.videoType) { searchParams.videoType = params.videoType; // movie, episode } // Add location-based search if (params.location && params.locationRadius) { searchParams.location = params.location; // lat,lng searchParams.locationRadius = params.locationRadius; // e.g., "10km" } // Add language filter if (params.language) { searchParams.relevanceLanguage = params.language; } // Add safe search if (params.safeSearch) { searchParams.safeSearch = params.safeSearch; // none, moderate, strict } // Add caption filter if (params.videoCaption) { searchParams.videoCaption = params.videoCaption; // closedCaption, none } // Add video quality filters if (params.videoDefinition) { searchParams.videoDefinition = params.videoDefinition; // high, standard } if (params.videoDimension) { searchParams.videoDimension = params.videoDimension; // 2d, 3d } if (params.videoLicense) { searchParams.videoLicense = params.videoLicense; // creativeCommon, youtube } // Add pagination if (params.pageToken) { searchParams.pageToken = params.pageToken; } // Perform search const searchResponse = await this.makeYouTubeRequest<any>( '/search', 'GET', undefined, searchParams, contextUser ); // Get detailed video information const videos: SocialPost[] = []; if (searchResponse.items && searchResponse.items.length > 0) { const videoIds = searchResponse.items.map((item: any) => item.id.videoId).join(','); const videosResponse = await this.makeYouTubeRequest<any>( '/videos', 'GET', undefined, { part: 'snippet,statistics,status,contentDetails', id: videoIds }, contextUser ); if (videosResponse.items) { for (const video of videosResponse.items) { videos.push(this.normalizePost(video)); } } } return { videos: videos, totalResults: searchResponse.pageInfo?.totalResults, nextPageToken: searchResponse.nextPageToken, prevPageToken: searchResponse.prevPageToken }; } /** * Analyze search results for insights */ private analyzeSearchResults(videos: SocialPost[]): any { if (videos.length === 0) { return { videoCount: 0, dateRange: null, performance: null, categories: {}, channels: {} }; } // Sort by date to find range const sortedByDate = [...videos].sort((a, b) => a.publishedAt.getTime() - b.publishedAt.getTime() ); // Calculate performance metrics let totalViews = 0; let totalLikes = 0; let totalComments = 0; const categories: Record<string, number> = {}; const channels: Record<string, { count: number; name: string }> = {}; for (const video of videos) { const platformData = video.platformSpecificData; const analytics = video.analytics; if (analytics) { totalViews += analytics.videoViews || 0; totalLikes += analytics.likes || 0; totalComments += analytics.comments || 0; } // Count by category const categoryId = platformData.categoryId; categories[categoryId] = (categories[categoryId] || 0) + 1; // Count by channel const channelId = video.profileId; if (!channels[channelId]) { channels[channelId] = { count: 0, name: platformData.channelTitle || channelId }; } channels[channelId].count++; } return { videoCount: videos.length, dateRange: { oldest: sortedByDate[0].publishedAt, newest: sortedByDate[sortedByDate.length - 1].publishedAt, spanDays: Math.ceil( (sortedByDate[sortedByDate.length - 1].publishedAt.getTime() - sortedByDate[0].publishedAt.getTime()) / (1000 * 60 * 60 * 24) ) }, performance: { totalViews: totalViews, totalLikes: totalLikes, totalComments: totalComments, averageViews: Math.round(totalViews / videos.length), averageLikes: Math.round(totalLikes / videos.length), averageComments: Math.round(totalComments / videos.length), topVideos: [...videos] .sort((a, b) => (b.analytics?.videoViews || 0) - (a.analytics?.videoViews || 0)) .slice(0, 5) .map(v => ({ id: v.id, title: v.platformSpecificData.title, views: v.analytics?.videoViews || 0, url: v.mediaUrls[0] })) }, categories: categories, channels: channels, durationBreakdown: this.analyzeDurations(videos), privacyBreakdown: this.analyzePrivacy(videos) }; } /** * Analyze video durations */ private analyzeDurations(videos: SocialPost[]): any { const durations = { short: 0, medium: 0, long: 0 }; for (const video of videos) { const duration = video.platformSpecificData.duration; if (duration) { const seconds = this.parseDuration(duration); if (seconds < 240) durations.short++; // < 4 minutes else if (seconds < 1200) durations.medium++; // 4-20 minutes else durations.long++; // > 20 minutes } } return durations; } /** * Analyze privacy status */ private analyzePrivacy(videos: SocialPost[]): any { const privacy = { public: 0, unlisted: 0, private: 0 }; for (const video of videos) { const status = video.platformSpecificData.privacyStatus || 'public'; privacy[status]++; } return privacy; } /** * 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: 'Query', Type: 'Input', Value: null }, { Name: 'ChannelID', Type: 'Input', Value: null }, { Name: 'StartDate', Type: 'Input', Value: null }, { Name: 'EndDate', Type: 'Input', Value: null }, { Name: 'VideoDuration', Type: 'Input', Value: null }, { Name: 'VideoType', Type: 'Input', Value: null }, { Name: 'OrderBy', Type: 'Input', Value: 'relevance' }, { Name: 'MaxResults', Type: 'Input', Value: 50 }, { Name: 'Location', Type: 'Input', Value: null }, { Name: 'LocationRadius', Type: 'Input', Value: null }, { Name: 'Language', Type: 'Input', Value: null }, { Name: 'SafeSearch', Type: 'Input', Value: 'moderate' }, { Name: 'VideoCaption', Type: 'Input', Value: null }, { Name: 'VideoDefinition', Type: 'Input', Value: null }, { Name: 'VideoDimension', Type: 'Input', Value: null }, { Name: 'VideoLicense', Type: 'Input', Value: null }, { Name: 'PageToken', Type: 'Input', Value: null }, { Name: 'IncludeHistorical', Type: 'Input', Value: true }, { Name: 'Videos', Type: 'Output', Value: null }, { Name: 'Summary', Type: 'Output', Value: null }, { Name: 'NextPageToken', Type: 'Output', Value: null } ]; return [...baseParams, ...specificParams]; } /** * Metadata about this action */ public get Description(): string { return 'Searches YouTube videos with comprehensive filtering options including historical content retrieval with date ranges'; } }