first commit
This commit is contained in:
243
scripts/batch-upload-animal-videos.cjs
Normal file
243
scripts/batch-upload-animal-videos.cjs
Normal file
@@ -0,0 +1,243 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const { exec } = require('child_process');
|
||||
const util = require('util');
|
||||
const execPromise = util.promisify(exec);
|
||||
|
||||
// 数据库连接
|
||||
const db = new sqlite3.Database('goguryeo_video.db');
|
||||
|
||||
// 视频文件目录
|
||||
const VIDEO_DIR = './video';
|
||||
const UPLOAD_VIDEO_DIR = './uploads/videos';
|
||||
const UPLOAD_COVER_DIR = './uploads/covers';
|
||||
|
||||
// 确保上传目录存在
|
||||
if (!fs.existsSync(UPLOAD_VIDEO_DIR)) {
|
||||
fs.mkdirSync(UPLOAD_VIDEO_DIR, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(UPLOAD_COVER_DIR)) {
|
||||
fs.mkdirSync(UPLOAD_COVER_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// 动物分类映射
|
||||
const animalCategoryMap = {
|
||||
'羊': { category: 5, tags: ['萌宠', '小羊', '可爱', '动物'] },
|
||||
'老虎': { category: 5, tags: ['萌宠', '老虎', '动物园', '可爱'] },
|
||||
'荷兰猪': { category: 5, tags: ['萌宠', '荷兰猪', '可爱'] },
|
||||
'兔子': { category: 5, tags: ['萌宠', '兔子', '可爱'] },
|
||||
'企鹅': { category: 5, tags: ['萌宠', '企鹅', '可爱'] },
|
||||
'牛': { category: 5, tags: ['动物', '小牛', '农村生活', '可爱'] },
|
||||
'猴子': { category: 5, tags: ['猴子', '可爱', '动物'] },
|
||||
'马': { category: 5, tags: ['萌宠', '矮马', '可爱'] },
|
||||
'水獭': { category: 5, tags: ['水獭', '萌宠', '治愈', '可爱'] },
|
||||
'鸟': { category: 5, tags: ['可爱', '治愈', '萌宠'] },
|
||||
'狗': { category: 5, tags: ['宠物', '狗子', '萌宠'] },
|
||||
'象': { category: 5, tags: ['小象', '萌宠', '可爱'] },
|
||||
'猪': { category: 5, tags: ['小香猪', '小猪', '可爱'] },
|
||||
'猫': { category: 5, tags: ['小猫咪', '萌宠', '可爱', '治愈'] }
|
||||
};
|
||||
|
||||
// 根据文件名识别动物类型
|
||||
function identifyAnimalType(filename) {
|
||||
for (const [animal, config] of Object.entries(animalCategoryMap)) {
|
||||
if (filename.includes(animal)) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
// 默认分类为民俗风情
|
||||
return { category: 5, tags: ['萌宠', '可爱', '动物'] };
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
function generateUniqueFilename(originalName) {
|
||||
const timestamp = Date.now();
|
||||
const randomStr = Math.random().toString(36).substring(2, 15);
|
||||
const ext = path.extname(originalName);
|
||||
return `${timestamp}_${randomStr}${ext}`;
|
||||
}
|
||||
|
||||
// 获取视频时长
|
||||
async function getVideoDuration(videoPath) {
|
||||
try {
|
||||
const { stdout } = await execPromise(`ffprobe -v quiet -show_entries format=duration -of csv=p=0 "${videoPath}"`);
|
||||
return Math.round(parseFloat(stdout.trim()));
|
||||
} catch (error) {
|
||||
console.log(`无法获取视频时长: ${videoPath}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成视频封面
|
||||
async function generateThumbnail(videoPath, outputPath) {
|
||||
try {
|
||||
await execPromise(`ffmpeg -i "${videoPath}" -ss 00:00:01 -vframes 1 -y "${outputPath}"`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(`生成封面失败: ${videoPath}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 插入视频到数据库
|
||||
function insertVideo(videoData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = `
|
||||
INSERT INTO videos (
|
||||
title, description, video_url, file_path, cover_url, cover_image,
|
||||
duration, file_size, category, tags, user_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
db.run(sql, [
|
||||
videoData.title,
|
||||
videoData.description,
|
||||
videoData.video_url,
|
||||
videoData.file_path,
|
||||
videoData.cover_url,
|
||||
videoData.cover_image,
|
||||
videoData.duration,
|
||||
videoData.file_size,
|
||||
videoData.category,
|
||||
videoData.tags,
|
||||
1 // admin user_id
|
||||
], function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(this.lastID);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 处理单个视频文件
|
||||
async function processVideo(filename) {
|
||||
try {
|
||||
const originalPath = path.join(VIDEO_DIR, filename);
|
||||
const stats = fs.statSync(originalPath);
|
||||
|
||||
// 生成新文件名
|
||||
const newVideoName = generateUniqueFilename(filename);
|
||||
const newCoverName = newVideoName.replace('.mp4', '_cover.jpg');
|
||||
|
||||
const newVideoPath = path.join(UPLOAD_VIDEO_DIR, newVideoName);
|
||||
const newCoverPath = path.join(UPLOAD_COVER_DIR, newCoverName);
|
||||
|
||||
// 复制视频文件
|
||||
fs.copyFileSync(originalPath, newVideoPath);
|
||||
console.log(`复制视频: ${filename} -> ${newVideoName}`);
|
||||
|
||||
// 生成封面
|
||||
const thumbnailGenerated = await generateThumbnail(newVideoPath, newCoverPath);
|
||||
|
||||
// 获取视频时长
|
||||
const duration = await getVideoDuration(newVideoPath);
|
||||
|
||||
// 识别动物类型和分类
|
||||
const animalConfig = identifyAnimalType(filename);
|
||||
|
||||
// 清理标题(移除文件扩展名和平台标识)
|
||||
const title = filename
|
||||
.replace('.mp4', '')
|
||||
.replace('-快手', '')
|
||||
.trim();
|
||||
|
||||
// 准备视频数据
|
||||
const videoData = {
|
||||
title: title,
|
||||
description: `可爱的动物视频:${title}`,
|
||||
video_url: `/uploads/videos/${newVideoName}`,
|
||||
file_path: `/uploads/videos/${newVideoName}`,
|
||||
cover_url: thumbnailGenerated ? `/uploads/covers/${newCoverName}` : null,
|
||||
cover_image: thumbnailGenerated ? `/uploads/covers/${newCoverName}` : null,
|
||||
duration: duration,
|
||||
file_size: stats.size,
|
||||
category: animalConfig.category,
|
||||
tags: animalConfig.tags.join(','),
|
||||
};
|
||||
|
||||
// 插入数据库
|
||||
const videoId = await insertVideo(videoData);
|
||||
console.log(`视频上传成功: ${title} (ID: ${videoId})`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
videoId: videoId,
|
||||
title: title,
|
||||
category: animalConfig.category
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error(`处理视频失败 ${filename}:`, error.message);
|
||||
return {
|
||||
success: false,
|
||||
filename: filename,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
try {
|
||||
console.log('开始批量上传动物视频...');
|
||||
|
||||
// 读取视频目录
|
||||
const files = fs.readdirSync(VIDEO_DIR)
|
||||
.filter(file => file.endsWith('.mp4'));
|
||||
|
||||
console.log(`找到 ${files.length} 个视频文件`);
|
||||
|
||||
const results = [];
|
||||
const categoryStats = {};
|
||||
|
||||
// 处理每个视频文件
|
||||
for (const file of files) {
|
||||
console.log(`\n处理: ${file}`);
|
||||
const result = await processVideo(file);
|
||||
results.push(result);
|
||||
|
||||
if (result.success) {
|
||||
const category = result.category;
|
||||
categoryStats[category] = (categoryStats[category] || 0) + 1;
|
||||
}
|
||||
|
||||
// 添加延迟避免系统过载
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
// 统计结果
|
||||
const successful = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success).length;
|
||||
|
||||
console.log('\n=== 上传完成 ===');
|
||||
console.log(`成功: ${successful} 个`);
|
||||
console.log(`失败: ${failed} 个`);
|
||||
|
||||
console.log('\n=== 分类统计 ===');
|
||||
for (const [categoryId, count] of Object.entries(categoryStats)) {
|
||||
console.log(`分类 ${categoryId}: ${count} 个视频`);
|
||||
}
|
||||
|
||||
if (failed > 0) {
|
||||
console.log('\n=== 失败的文件 ===');
|
||||
results.filter(r => !r.success).forEach(r => {
|
||||
console.log(`${r.filename}: ${r.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('批量上传失败:', error);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 运行脚本
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { main, processVideo };
|
||||
Reference in New Issue
Block a user