知识库首页 seo-llm 资料 fetch-longcat-tutorials.ts.txt

fetch longcat tutorials.ts

本地来源:seo-llm/raw/seo知识库/TXT整理/seo方法论/seo-knowledge-base/Scripts-for-Youtube/fetch-longcat-tutorials.ts.txt

/**
 * 优化版:专注采集 LongCat Video/Image AI 工具的教程视频
 * 筛选标准:
 * - 时长 > 3 分钟
 * - 包含教程关键词
 * - 排除游戏、娱乐类视频
 *
 * 运行方式: pnpm tsx "SEO/Scripts for Youtube/fetch-longcat-tutorials.ts"
 */

import fs from 'node:fs';
import path from 'node:path';
import { config } from 'dotenv';
import { getSubtitles } from 'youtube-caption-extractor';

// 加载环境变量
config({ path: '.env.development' });

// YouTube API 配置
const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY;
const YOUTUBE_API_BASE = 'https://www.googleapis.com/youtube/v3';

// 延迟函数
function delay(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// 解析ISO 8601时长为秒数
function parseDurationToSeconds(isoDuration: string): number {
  const match = isoDuration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
  if (!match) return 0;

  const hours = Number.parseInt(match[1] || '0', 10);
  const minutes = Number.parseInt(match[2] || '0', 10);
  const seconds = Number.parseInt(match[3] || '0', 10);

  return hours * 3600 + minutes * 60 + seconds;
}

// 格式化时长
function formatDuration(isoDuration: string): string {
  const match = isoDuration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
  if (!match) return isoDuration;

  const hours = match[1] ? `${match[1]}:` : '';
  const minutes = match[2] ? match[2].padStart(hours ? 2 : 1, '0') : '00';
  const seconds = match[3] ? match[3].padStart(2, '0') : '00';

  return `${hours}${minutes}:${seconds}`;
}

// 格式化播放量
function formatViewCount(count: string): string {
  const num = Number.parseInt(count, 10);
  if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
  if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
  return count;
}

// 检查视频是否相关(排除游戏、娱乐类)
function isRelevantVideo(title: string, description: string): boolean {
  const lowerTitle = title.toLowerCase();
  const lowerDesc = description.toLowerCase();

  // 排除关键词(游戏、纯娱乐)
  const excludeKeywords = [
    'fancade',
    'game level',
    'gaming',
    'speed level',
    'cat dance',
    'funny cat',
    'cute cat',
    'meme',
    'compilation',
    'shorts',
  ];

  for (const keyword of excludeKeywords) {
    if (lowerTitle.includes(keyword)) {
      return false;
    }
  }

  // 必须包含的关键词之一(AI工具相关)
  const requiredKeywords = [
    'longcat',
    'comfyui',
    'tutorial',
    'workflow',
    'guide',
    'how to',
    'avatar',
    'ai video',
    'image generator',
  ];

  const hasRequired = requiredKeywords.some(
    (keyword) => lowerTitle.includes(keyword) || lowerDesc.includes(keyword)
  );

  return hasRequired;
}

// 搜索视频(支持分页和时长过滤)
async function searchVideos(keyword: string, maxResults = 50) {
  console.log(`\n🔍 搜索关键词: "${keyword}"`);
  console.log(`📊 最大结果数: ${maxResults} (筛选后)\n`);

  if (!YOUTUBE_API_KEY) {
    throw new Error('YOUTUBE_API_KEY 环境变量未设置');
  }

  let allItems: any[] = [];
  let pageToken: string | undefined = undefined;
  let pageCount = 0;
  let totalFetched = 0;

  // 循环获取直到达到目标数量或无更多结果
  while (allItems.length < maxResults && pageCount < 10) {
    pageCount++;
    const perPageResults = 50; // YouTube API 每次最多50个

    console.log(`📄 获取第 ${pageCount} 页 (每页 ${perPageResults} 个)...`);

    const params: any = {
      part: 'snippet',
      q: keyword,
      type: 'video',
      maxResults: perPageResults.toString(),
      order: 'relevance',
      videoDuration: 'medium', // 4-20分钟
      key: YOUTUBE_API_KEY,
    };

    if (pageToken) {
      params.pageToken = pageToken;
    }

    const searchResponse = await fetch(
      `${YOUTUBE_API_BASE}/search?${new URLSearchParams(params)}`
    );

    if (!searchResponse.ok) {
      const error = await searchResponse.json();
      throw new Error(`YouTube 搜索失败: ${JSON.stringify(error)}`);
    }

    const searchData = await searchResponse.json();

    if (!searchData.items || searchData.items.length === 0) {
      console.log('⚠️  没有更多视频了');
      break;
    }

    totalFetched += searchData.items.length;
    allItems = allItems.concat(searchData.items);
    console.log(`   ✓ 累计获取 ${totalFetched} 个视频 (筛选前)`);

    if (!searchData.nextPageToken) {
      console.log('✅ 已获取所有结果(没有更多页面)\n');
      break;
    }

    pageToken = searchData.nextPageToken;
    await delay(1000);
  }

  if (allItems.length === 0) {
    return { videos: [], totalResults: 0 };
  }

  // 批量获取视频详细信息
  console.log(`\n📊 获取视频详情和筛选...`);
  const allVideosData: any[] = [];

  for (let i = 0; i < allItems.length; i += 50) {
    const batch = allItems.slice(i, i + 50);
    const videoIds = batch.map((item: any) => item.id.videoId).join(',');

    console.log(
      `   处理 ${i + 1}-${Math.min(i + 50, allItems.length)}/${allItems.length}...`
    );

    const videosResponse = await fetch(
      `${YOUTUBE_API_BASE}/videos?${new URLSearchParams({
        part: 'snippet,contentDetails,statistics',
        id: videoIds,
        key: YOUTUBE_API_KEY,
      })}`
    );

    if (!videosResponse.ok) {
      const error = await videosResponse.json();
      throw new Error(`获取视频详情失败: ${JSON.stringify(error)}`);
    }

    const videosData = await videosResponse.json();
    allVideosData.push(...videosData.items);

    if (i + 50 < allItems.length) {
      await delay(1000);
    }
  }

  console.log('✅ 获取所有视频详情完成\n');

  // 筛选和格式化视频数据
  const videos = allVideosData
    .filter((item: any) => {
      // 1. 时长筛选:> 3 分钟 (180秒)
      const durationSeconds = parseDurationToSeconds(
        item.contentDetails.duration
      );
      if (durationSeconds < 180) {
        return false;
      }

      // 2. 内容相关性筛选
      if (!isRelevantVideo(item.snippet.title, item.snippet.description)) {
        return false;
      }

      return true;
    })
    .map((item: any) => ({
      id: item.id,
      title: item.snippet.title,
      channelTitle: item.snippet.channelTitle,
      channelId: item.snippet.channelId,
      publishedAt: item.snippet.publishedAt,
      description: item.snippet.description,
      thumbnailUrl:
        item.snippet.thumbnails.high?.url ||
        item.snippet.thumbnails.medium?.url,
      duration: formatDuration(item.contentDetails.duration),
      durationSeconds: parseDurationToSeconds(item.contentDetails.duration),
      viewCount: formatViewCount(item.statistics.viewCount || '0'),
      viewCountRaw: Number.parseInt(item.statistics.viewCount || '0', 10),
      likeCount: Number.parseInt(item.statistics.likeCount || '0', 10),
      commentCount: Number.parseInt(item.statistics.commentCount || '0', 10),
      url: `https://www.youtube.com/watch?v=${item.id}`,
    }));

  console.log(`🎯 筛选结果: ${videos.length} 个符合条件的视频\n`);

  return {
    keyword,
    searchDate: new Date().toISOString(),
    totalResults: videos.length,
    videos,
  };
}

// 提取单个视频脚本
async function extractScript(videoId: string) {
  try {
    console.log(`  → 提取视频: ${videoId}`);

    const captions = await getSubtitles({ videoID: videoId });

    let script = '';
    let hasTranscript = false;

    if (captions && captions.length > 0) {
      script = captions
        .map((item) => item.text)
        .join(' ')
        .replace(/\s+/g, ' ')
        .trim();
      hasTranscript = true;
    }

    console.log(
      `  ✓ ${videoId}: ${hasTranscript ? `成功 (${script.length} 字符)` : '无字幕'}`
    );

    return {
      videoId,
      success: true,
      hasTranscript,
      script,
      scriptLength: script.length,
      extractedAt: new Date().toISOString(),
    };
  } catch (error: any) {
    console.log(`  ✗ ${videoId}: 失败 - ${error.message}`);
    return {
      videoId,
      success: false,
      hasTranscript: false,
      script: '',
      error: error.message,
    };
  }
}

// 批量提取脚本
async function batchExtractScripts(videoIds: string[]) {
  console.log(`\n📝 开始批量提取脚本 (${videoIds.length} 个视频)\n`);

  const results = [];

  for (let i = 0; i < videoIds.length; i++) {
    const videoId = videoIds[i];
    console.log(`[${i + 1}/${videoIds.length}]`);

    const result = await extractScript(videoId);
    results.push(result);

    if (i < videoIds.length - 1) {
      await delay(1000);
    }
  }

  const successCount = results.filter((r) => r.success).length;
  const hasTranscriptCount = results.filter((r) => r.hasTranscript).length;
  const failedCount = results.filter((r) => !r.success).length;

  console.log('\n✅ 提取完成:');
  console.log(`   成功: ${successCount} 个`);
  console.log(`   有字幕: ${hasTranscriptCount} 个`);
  console.log(`   失败: ${failedCount} 个\n`);

  return results;
}

// 保存结果到文件
function saveResults(data: any, filename: string, targetDir?: string) {
  const dataDir = targetDir ?? path.join(process.cwd(), 'data/seo');

  if (!fs.existsSync(dataDir)) {
    fs.mkdirSync(dataDir, { recursive: true });
  }

  const filePath = path.join(dataDir, filename);
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');

  console.log(`💾 结果已保存到: ${filePath}`);
  console.log(
    `📦 文件大小: ${(fs.statSync(filePath).size / 1024).toFixed(2)} KB\n`
  );

  return filePath;
}

// 主函数
async function main() {
  try {
    console.log('🚀 开始获取 LongCat 教程视频 (优化版)\n');
    console.log('='.repeat(60));

    // 优化的教程类关键词
    const keywords = [
      'longcat video tutorial',
      'longcat comfyui workflow',
      'longcat avatar tutorial',
      'longcat video guide',
      'how to use longcat video',
      'longcat image tutorial',
      'comfyui longcat',
      'longcat ai video',
    ];

    console.log(`\n📋 搜索策略: 使用 ${keywords.length} 个教程类关键词`);
    console.log(`🎯 筛选标准: 时长 > 3 分钟, 排除游戏/娱乐类\n`);

    const allVideos: any[] = [];
    const videoIdsSet = new Set<string>();

    for (const keyword of keywords) {
      console.log(`\n${'='.repeat(60)}`);
      const searchResult = await searchVideos(keyword, 50);

      let newVideos = 0;
      for (const video of searchResult.videos) {
        if (!videoIdsSet.has(video.id)) {
          videoIdsSet.add(video.id);
          allVideos.push(video);
          newVideos++;
        }
      }

      console.log(
        `   ℹ️  新增 ${newVideos} 个不重复视频,累计 ${allVideos.length} 个`
      );

      await delay(2000);
    }

    console.log(`\n${'='.repeat(60)}`);
    console.log(
      `\n✅ 所有关键词搜索完成,共获取 ${allVideos.length} 个教程视频\n`
    );

    if (allVideos.length === 0) {
      console.log('没有找到相关视频,程序结束。');
      return;
    }

    // 批量提取脚本
    const videoIds = allVideos.map((v) => v.id);
    const extractResults = await batchExtractScripts(videoIds);

    // 合并结果
    const completeData = {
      keywords: keywords,
      searchDate: new Date().toISOString(),
      totalVideos: allVideos.length,
      videos: allVideos.map((video) => {
        const extractResult = extractResults.find(
          (r) => r.videoId === video.id
        );
        return {
          ...video,
          script: extractResult?.script || '',
          hasTranscript: extractResult?.hasTranscript || false,
          scriptLength: extractResult?.scriptLength || 0,
          extractedAt: extractResult?.extractedAt || null,
          extractError: extractResult?.error || null,
        };
      }),
      statistics: {
        totalVideos: allVideos.length,
        successfulExtracts: extractResults.filter((r) => r.success).length,
        videosWithTranscript: extractResults.filter((r) => r.hasTranscript)
          .length,
        failedExtracts: extractResults.filter((r) => !r.success).length,
        highPriority: allVideos.filter((v) => v.viewCountRaw > 10000).length,
        mediumPriority: allVideos.filter(
          (v) => v.viewCountRaw > 1000 && v.viewCountRaw <= 10000
        ).length,
      },
      exportedAt: new Date().toISOString(),
    };

    // 保存结果
    console.log('='.repeat(60));
    saveResults(
      completeData,
      `longcat-tutorials-${Date.now()}.json`,
      path.join(process.cwd(), 'data/seo')
    );
    saveResults(
      completeData,
      'longcat-tutorials.json',
      path.join(process.cwd(), 'SEO/Videos')
    );

    // 打印统计信息
    console.log('📊 统计信息:');
    console.log(`   总视频数: ${completeData.statistics.totalVideos}`);
    console.log(`   成功提取: ${completeData.statistics.successfulExtracts}`);
    console.log(`   有字幕: ${completeData.statistics.videosWithTranscript}`);
    console.log(`   失败: ${completeData.statistics.failedExtracts}`);
    console.log(
      `   高优先级 (>10K播放): ${completeData.statistics.highPriority}`
    );
    console.log(
      `   中优先级 (1K-10K): ${completeData.statistics.mediumPriority}`
    );

    // Top 10
    console.log('\n📈 播放量 Top 10 视频:');
    const topVideos = completeData.videos
      .sort((a, b) => b.viewCountRaw - a.viewCountRaw)
      .slice(0, 10);

    for (let i = 0; i < topVideos.length; i++) {
      const video = topVideos[i];
      console.log(
        `   ${i + 1}. ${video.title.substring(0, 80)}${video.title.length > 80 ? '...' : ''}`
      );
      console.log(`      播放量: ${video.viewCount} | ${video.url}`);
    }

    console.log('\n✨ 全部完成!');
  } catch (error) {
    console.error('\n❌ 错误:', error);
    process.exit(1);
  }
}

// 运行
main();

本文档为站内渲染。原始文件本地路径:saas/source/seo-llm/raw-seo知识库-TXT整理-seo方法论-seo-knowledge-base-Scripts-for-Youtu-999970.txt(仅本地保留,不入库不部署)