gongxiangp2/scripts/retranscode-all-videos.js

217 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 批量转码所有视频素材脚本
* 用途:一次性任务,将所有视频素材重新转码为新标准(720P + H.265)
*
* 使用方法:
* 1. 确保服务器正在运行
* 2. 在浏览器中登录管理员账号,获取 session cookie
* 3. 运行: node scripts/retranscode-all-videos.js
*
* 或者直接在管理员页面使用批量转码功能
*/
const https = require('https');
const http = require('http');
// 配置
const config = {
baseUrl: process.env.API_BASE_URL || 'http://localhost:3000',
// 如果需要认证,请设置 session cookie
// cookie: 'next-auth.session-token=YOUR_SESSION_TOKEN',
cookie: process.env.ADMIN_SESSION_COOKIE || '',
batchSize: 1000, // 每批处理数量
};
/**
* 发送 HTTP 请求
*/
function request(options, data) {
return new Promise((resolve, reject) => {
const url = new URL(options.url || config.baseUrl + options.path);
const isHttps = url.protocol === 'https:';
const client = isHttps ? https : http;
const reqOptions = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname + url.search,
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
...(config.cookie ? { 'Cookie': config.cookie } : {}),
...(options.headers || {}),
},
};
const req = client.request(reqOptions, (res) => {
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => {
try {
const json = JSON.parse(body);
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve({ status: res.statusCode, data: json });
} else {
reject(new Error(`HTTP ${res.statusCode}: ${json.error || body}`));
}
} catch (e) {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve({ status: res.statusCode, data: body });
} else {
reject(new Error(`HTTP ${res.statusCode}: ${body}`));
}
}
});
});
req.on('error', reject);
if (data) {
req.write(JSON.stringify(data));
}
req.end();
});
}
/**
* 主函数
*/
async function main() {
console.log('==========================================');
console.log('批量转码所有视频素材');
console.log('==========================================');
console.log(`API URL: ${config.baseUrl}`);
console.log(`批次大小: ${config.batchSize}`);
console.log('');
// 第一步:统计需要转码的素材数量
console.log('📊 第一步:统计需要转码的素材数量...');
try {
const dryRunResponse = await request({
method: 'POST',
path: '/api/admin/video-retranscode',
}, {
limit: 0, // 0 表示不限制,获取所有匹配的素材
status: 'any', // 转码所有状态的视频
dryRun: true,
});
const totalCount = dryRunResponse.data.matched || 0;
console.log(`✓ 找到 ${totalCount} 个视频素材需要转码`);
if (totalCount === 0) {
console.log('没有找到需要转码的视频素材');
return;
}
if (dryRunResponse.data.sample && dryRunResponse.data.sample.length > 0) {
console.log('\n前10个素材预览:');
dryRunResponse.data.sample.forEach((m, i) => {
console.log(` ${i + 1}. ID: ${m.id.substring(0, 20)}..., 产品: ${m.productModel || 'N/A'}, 状态: ${m.status || 'null'}`);
});
}
// 确认执行
console.log('\n⚠️ 警告:即将开始批量转码所有视频素材');
console.log(' 转码是异步执行的,会进入队列处理');
console.log(' 转码完成后,旧的预览文件会自动清理');
console.log('\n按 Ctrl+C 取消,或等待 5 秒后自动开始...');
await new Promise(resolve => setTimeout(resolve, 5000));
// 第二步:分批执行转码
console.log('\n🚀 开始批量转码...');
const totalBatches = Math.ceil(totalCount / config.batchSize);
console.log(`预计需要 ${totalBatches} 个批次\n`);
let totalQueued = 0;
let totalFailed = 0;
let processedCount = 0;
for (let batch = 1; batch <= totalBatches; batch++) {
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(`批次 ${batch}/${totalBatches}`);
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
try {
const response = await request({
method: 'POST',
path: '/api/admin/video-retranscode',
}, {
limit: config.batchSize,
status: 'any',
dryRun: false,
});
const queued = response.data.queued || 0;
const failed = response.data.failed || 0;
const matched = response.data.matched || 0;
totalQueued += queued;
totalFailed += failed;
processedCount += matched;
console.log(`✓ 批次 ${batch} 完成: 已排队 ${queued} 个,失败 ${failed} 个`);
if (response.data.details && response.data.details.length > 0) {
const failedDetails = response.data.details.filter(d => !d.ok).slice(0, 5);
if (failedDetails.length > 0) {
console.log('\n失败详情(前5个):');
failedDetails.forEach(d => {
console.log(` - ID: ${d.id.substring(0, 20)}..., 状态: ${d.status}, 错误: ${d.message || 'N/A'}`);
});
}
}
// 如果这一批匹配的数量少于批次大小,说明已经处理完所有素材
if (matched < config.batchSize) {
console.log('\n已处理完所有素材');
break;
}
// 批次间稍作延迟,避免请求过快
if (batch < totalBatches) {
console.log('\n等待 2 秒后继续下一批次...\n');
await new Promise(resolve => setTimeout(resolve, 2000));
}
} catch (error) {
console.error(`❌ 批次 ${batch} 失败:`, error.message);
// 继续处理下一批次
continue;
}
}
// 总结
console.log('\n==========================================');
console.log('批量转码完成');
console.log('==========================================');
console.log(`✓ 总计已排队: ${totalQueued} 个`);
if (totalFailed > 0) {
console.log(`⚠️ 总计失败: ${totalFailed} 个`);
}
console.log('\n📝 说明:');
console.log(' - 转码是异步执行的,会进入队列处理');
console.log(' - 实际并发数由服务器配置控制(VIDEO_TRANSCODE_MAX_CONCURRENT)');
console.log(' - 转码成功后,旧的预览文件会自动清理');
console.log(' - 可以在管理员页面查看转码进度和状态');
console.log('');
} catch (error) {
console.error('❌ 执行失败:', error.message);
if (error.message.includes('403') || error.message.includes('无权限')) {
console.error('\n提示:需要管理员权限。请:');
console.error(' 1. 在浏览器中登录管理员账号');
console.error(' 2. 获取 session cookie');
console.error(' 3. 设置环境变量: ADMIN_SESSION_COOKIE="next-auth.session-token=YOUR_TOKEN"');
console.error(' 或者直接在管理员页面使用批量转码功能');
}
process.exit(1);
}
}
// 运行主函数
main().catch(console.error);