fetch longcat videos.ts
本地来源:seo-llm/raw/seo知识库/TXT整理/seo方法论/seo-knowledge-base/Scripts-for-Youtube/fetch-longcat-videos.ts.txt
/**
* 自动获取 longcat-video / longcat-image 相关的所有 YouTube 视频和脚本内容
* 保存为 JSON 格式到本地
*
* 运行方式: pnpm tsx "SEO/Scripts for Youtube/fetch-longcat-videos.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));
}
// 格式化时长
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;
}
// 搜索视频(支持分页)
async function searchVideos(keyword: string, maxResults = 100) {
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;
// 循环获取所有页面
while (allItems.length < maxResults) {
pageCount++;
const perPageResults = Math.min(50, maxResults - allItems.length);
console.log(`📄 获取第 ${pageCount} 页 (每页 ${perPageResults} 个)...`);
const params: any = {
part: 'snippet',
q: keyword,
type: 'video',
maxResults: perPageResults.toString(),
order: 'relevance',
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;
}
allItems = allItems.concat(searchData.items);
console.log(` ✓ 累计获取 ${allItems.length} 个视频`);
if (!searchData.nextPageToken) {
console.log('✅ 已获取所有结果(没有更多页面)\n');
break;
}
pageToken = searchData.nextPageToken;
await delay(1000);
}
console.log(`\n✅ 总共找到 ${allItems.length} 个视频\n`);
if (allItems.length === 0) {
return { videos: [], totalResults: 0 };
}
// 批量获取视频详细信息
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 videosData = { items: allVideosData };
const videos = videosData.items.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),
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}`,
}));
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-video / longcat-image 相关视频和脚本\n');
console.log('='.repeat(60));
// 搜索关键词
const keywords = [
'longcat-video',
'longcat video',
'longcat-image',
'longcat image',
'longcat video tutorial',
'longcat image generator',
'longcat video maker',
'longcat ai video',
];
console.log(`\n📋 搜索策略: 使用 ${keywords.length} 个相关关键词\n`);
const allVideos: any[] = [];
const videoIdsSet = new Set<string>();
for (const keyword of keywords) {
console.log(`\n${'='.repeat(60)}`);
const searchResult = await searchVideos(keyword, 100);
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,
},
exportedAt: new Date().toISOString(),
};
// 保存结果
console.log('='.repeat(60));
saveResults(
completeData,
`longcat-complete-${Date.now()}.json`,
path.join(process.cwd(), 'data/seo')
);
saveResults(
completeData,
'longcat-combined.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}`);
// 显示播放量Top 10
const topVideos = allVideos
.sort((a, b) => b.viewCountRaw - a.viewCountRaw)
.slice(0, 10);
console.log('\n📈 播放量 Top 10 视频:');
topVideos.forEach((v, i) => {
console.log(` ${i + 1}. ${v.title}`);
console.log(` 播放量: ${v.viewCount} | ${v.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-693daf.txt(仅本地保留,不入库不部署)