910 lines
33 KiB
TypeScript
910 lines
33 KiB
TypeScript
import {Files} from "../file/main";
|
|
import {Log} from "../log/main";
|
|
import {AppEnv} from "../env";
|
|
import {spawn} from "child_process";
|
|
import * as fs from "fs";
|
|
import * as path from "path";
|
|
import {getPythonPath} from "../../lib/python-util";
|
|
import {getPythonScriptPath, getFFmpegExecutablePath, resourceExists} from "../../lib/resource-path";
|
|
import {spawnPython, createTempConfig, cleanupTempConfig} from "../../lib/python-spawn-util";
|
|
import {normalizePath, normalizeOutputPath} from "../../lib/path-util";
|
|
import DB from "../db/main";
|
|
|
|
const TAG = "SubtitleCoverGenerator";
|
|
|
|
interface GenerateRequest {
|
|
videoPath: string;
|
|
scriptText: string;
|
|
subtitleStyle: string;
|
|
coverStyle: string;
|
|
// 新增参数
|
|
coverText?: string;
|
|
coverTextPosition?: string;
|
|
coverTextColor?: string;
|
|
coverTextSize?: number;
|
|
outlineColor?: string;
|
|
outlineWidth?: number;
|
|
blurRadius?: number;
|
|
zoomLevel?: number;
|
|
}
|
|
|
|
interface GenerateResult {
|
|
success: boolean;
|
|
coverPath?: string;
|
|
srtPath?: string;
|
|
subtitles?: Array<{
|
|
index: number;
|
|
text: string;
|
|
start_time: number;
|
|
end_time: number;
|
|
style: string;
|
|
}>;
|
|
error?: string;
|
|
message: string;
|
|
}
|
|
|
|
class SubtitleCoverGenerator {
|
|
private getPythonScriptPath(): string {
|
|
// Python脚本路径
|
|
return getPythonScriptPath('subtitle_cover_generator_simple.py');
|
|
}
|
|
|
|
// 移除重复的 createTempConfig 方法,使用 python-spawn-util 中的统一实现
|
|
|
|
/**
|
|
* 执行Python命令
|
|
* ⚠️ 只使用程序自带的 Python,不依赖系统环境
|
|
*/
|
|
private async execPython(command: string, timeoutMs: number = 300000): Promise<string> {
|
|
// 解析command字符串为参数数组
|
|
const args = command.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
const parsedArgs = args.map(arg => arg.replace(/^"|"$/g, '')); // 移除引号
|
|
|
|
// 使用统一的 Python 调用工具(支持中文路径)
|
|
return spawnPython(parsedArgs, {
|
|
timeout: timeoutMs,
|
|
addFFmpegToPath: true
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 生成字幕和封面
|
|
* @param request 生成请求参数
|
|
* @returns 生成结果
|
|
*/
|
|
async generateSubtitleAndCover(request: GenerateRequest): Promise<GenerateResult> {
|
|
try {
|
|
Log.info(TAG, `Starting generation for video: ${request.videoPath}`);
|
|
|
|
// 验证输入参数
|
|
if (!request.videoPath || !request.scriptText) {
|
|
throw new Error("Video path and script text are required");
|
|
}
|
|
|
|
if (!fs.existsSync(request.videoPath)) {
|
|
throw new Error(`Video file not found: ${request.videoPath}`);
|
|
}
|
|
|
|
const safeVideoPath = normalizePath(request.videoPath);
|
|
|
|
const hubRoot = await Files.hubRoot();
|
|
Log.info(TAG, `Using hubRoot for output: ${hubRoot}`);
|
|
|
|
const outputDir = path.join(hubRoot, `subtitle_cover_${Date.now()}`);
|
|
if (!fs.existsSync(outputDir)) {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
}
|
|
|
|
const safeOutputDir = normalizeOutputPath(outputDir);
|
|
const safeHubRoot = normalizePath(hubRoot);
|
|
|
|
const config = {
|
|
videoPath: safeVideoPath,
|
|
scriptText: request.scriptText,
|
|
subtitleStyle: request.subtitleStyle || "default",
|
|
coverStyle: request.coverStyle || "default",
|
|
outputDir: safeOutputDir,
|
|
hubRoot: safeHubRoot,
|
|
// 新增参数
|
|
coverText: request.coverText || '',
|
|
coverTextPosition: request.coverTextPosition || 'bottom',
|
|
coverTextColor: request.coverTextColor || '#FFFFFF',
|
|
coverTextSize: request.coverTextSize || 50,
|
|
outlineColor: request.outlineColor || '#FFD700',
|
|
outlineWidth: request.outlineWidth || 3,
|
|
blurRadius: request.blurRadius || 15,
|
|
zoomLevel: request.zoomLevel || 1.0
|
|
};
|
|
|
|
const configPath = await createTempConfig(config);
|
|
Log.info(TAG, `Created config file: ${configPath}`);
|
|
|
|
// 执行Python脚本 (设置10分钟超时)
|
|
const pythonCommand = `"${this.getPythonScriptPath()}" "${configPath}"`;
|
|
Log.info(TAG, `Executing Python command with 10 minute timeout`);
|
|
|
|
const result = await this.execPython(pythonCommand, 600000); // 10分钟超时
|
|
|
|
Log.info(TAG, `Python execution completed`);
|
|
|
|
// 解析结果
|
|
let generateResult: GenerateResult;
|
|
try {
|
|
// 查找JSON结果(可能在输出的最后)
|
|
const lines = result.trim().split('\n');
|
|
const jsonLine = lines[lines.length - 1];
|
|
generateResult = JSON.parse(jsonLine);
|
|
} catch (parseError) {
|
|
Log.error(TAG, `Failed to parse Python result: ${parseError}`);
|
|
throw new Error(`Invalid result format`);
|
|
}
|
|
|
|
// 清理临时配置文件
|
|
cleanupTempConfig(configPath);
|
|
|
|
Log.info(TAG, `Generation completed: ${generateResult.message}`);
|
|
return generateResult;
|
|
|
|
} catch (error: any) {
|
|
Log.error(TAG, `Generation failed: ${error}`);
|
|
return {
|
|
success: false,
|
|
error: error.message || String(error),
|
|
message: `Generation failed: ${error.message || String(error)}`
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取支持的字幕样式列表
|
|
* @returns 字幕样式列表
|
|
*/
|
|
getSubtitleStyles(): Array<{value: string; label: string; description: string}> {
|
|
return [
|
|
{
|
|
value: "default",
|
|
label: "默认样式",
|
|
description: "白色字体,黑色描边"
|
|
},
|
|
{
|
|
value: "highlight",
|
|
label: "高亮样式",
|
|
description: "黄色高亮背景"
|
|
},
|
|
{
|
|
value: "card",
|
|
label: "卡片样式",
|
|
description: "圆角卡片背景"
|
|
},
|
|
{
|
|
value: "neon",
|
|
label: "霓虹样式",
|
|
description: "霓虹灯效果"
|
|
},
|
|
{
|
|
value: "minimal",
|
|
label: "极简样式",
|
|
description: "简洁现代风格"
|
|
}
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 获取支持的封面样式列表
|
|
* @returns 封面样式列表
|
|
*/
|
|
getCoverStyles(): Array<{value: string; label: string; description: string}> {
|
|
return [
|
|
{
|
|
value: "default",
|
|
label: "默认封面",
|
|
description: "标准视频封面"
|
|
},
|
|
{
|
|
value: "blur-bg",
|
|
label: "背景虚化",
|
|
description: "人物清晰,背景虚化"
|
|
},
|
|
{
|
|
value: "outline",
|
|
label: "人物描边",
|
|
description: "人物描边效果"
|
|
},
|
|
{
|
|
value: "gradient",
|
|
label: "渐变背景",
|
|
description: "渐变色背景"
|
|
},
|
|
{
|
|
value: "split",
|
|
label: "斜切设计",
|
|
description: "斜切色块设计"
|
|
}
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 重新生成封面
|
|
* @param request 重新生成请求参数
|
|
* @returns 生成结果
|
|
*/
|
|
async regenerateCover(request: {
|
|
videoPath: string;
|
|
scriptText: string;
|
|
coverStyle: string;
|
|
// 新增参数
|
|
coverText?: string;
|
|
coverTextPosition?: string;
|
|
coverTextColor?: string;
|
|
coverTextSize?: number;
|
|
outlineColor?: string;
|
|
outlineWidth?: number;
|
|
blurRadius?: number;
|
|
zoomLevel?: number;
|
|
}): Promise<GenerateResult> {
|
|
try {
|
|
Log.info(TAG, `Regenerating cover for video: ${request.videoPath}`);
|
|
|
|
// 验证输入参数
|
|
if (!request.videoPath) {
|
|
throw new Error("Video path is required");
|
|
}
|
|
|
|
if (!fs.existsSync(request.videoPath)) {
|
|
throw new Error(`Video file not found: ${request.videoPath}`);
|
|
}
|
|
|
|
const safeVideoPath = normalizePath(request.videoPath);
|
|
|
|
const hubRoot = await Files.hubRoot();
|
|
Log.info(TAG, `Using hubRoot for output: ${hubRoot}`);
|
|
|
|
const outputDir = path.join(hubRoot, `subtitle_cover_${Date.now()}`);
|
|
if (!fs.existsSync(outputDir)) {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
}
|
|
|
|
const safeOutputDir = normalizeOutputPath(outputDir);
|
|
const safeHubRoot = normalizePath(hubRoot);
|
|
|
|
const config = {
|
|
videoPath: safeVideoPath,
|
|
scriptText: request.scriptText,
|
|
subtitleStyle: "default",
|
|
coverStyle: request.coverStyle || "default",
|
|
outputDir: safeOutputDir,
|
|
hubRoot: safeHubRoot,
|
|
coverOnly: true,
|
|
// 新增参数
|
|
coverText: request.coverText || '',
|
|
coverTextPosition: request.coverTextPosition || 'bottom',
|
|
coverTextColor: request.coverTextColor || '#FFFFFF',
|
|
coverTextSize: request.coverTextSize || 50,
|
|
outlineColor: request.outlineColor || '#FFD700',
|
|
outlineWidth: request.outlineWidth || 3,
|
|
blurRadius: request.blurRadius || 15,
|
|
zoomLevel: request.zoomLevel || 1.0
|
|
};
|
|
|
|
const configPath = await createTempConfig(config);
|
|
Log.info(TAG, `Created config file for cover regeneration: ${configPath}`);
|
|
|
|
// 执行Python脚本
|
|
const pythonCommand = `"${this.getPythonScriptPath()}" "${configPath}"`;
|
|
Log.info(TAG, `Executing Python command for cover regeneration`);
|
|
|
|
const result = await this.execPython(pythonCommand);
|
|
|
|
Log.info(TAG, `Python execution completed for cover regeneration`);
|
|
|
|
// 解析结果
|
|
let generateResult: GenerateResult;
|
|
try {
|
|
// 查找JSON结果(可能在输出的最后)
|
|
const lines = result.trim().split('\n');
|
|
const jsonLine = lines[lines.length - 1];
|
|
generateResult = JSON.parse(jsonLine);
|
|
} catch (parseError) {
|
|
Log.error(TAG, `Failed to parse Python result: ${parseError}`);
|
|
throw new Error(`Invalid result format`);
|
|
}
|
|
|
|
// 清理临时配置文件
|
|
cleanupTempConfig(configPath);
|
|
|
|
Log.info(TAG, `Cover regeneration completed: ${generateResult.message}`);
|
|
return generateResult;
|
|
|
|
} catch (error: any) {
|
|
Log.error(TAG, `Cover regeneration failed: ${error}`);
|
|
return {
|
|
success: false,
|
|
error: error.message || String(error),
|
|
message: `Cover regeneration failed: ${error.message || String(error)}`
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 生成实时封面预览
|
|
* @param request 预览请求参数
|
|
* @returns 预览结果
|
|
*/
|
|
async generateCoverPreviews(request: {
|
|
videoPath: string;
|
|
scriptText?: string;
|
|
}): Promise<{
|
|
success: boolean;
|
|
previews?: Array<{
|
|
style: string;
|
|
url: string;
|
|
}>;
|
|
error?: string;
|
|
message: string;
|
|
}> {
|
|
try {
|
|
Log.info(TAG, `Generating cover previews for video: ${request.videoPath}`);
|
|
|
|
// 验证输入参数
|
|
if (!request.videoPath) {
|
|
throw new Error("Video path is required");
|
|
}
|
|
|
|
if (!fs.existsSync(request.videoPath)) {
|
|
throw new Error(`Video file not found: ${request.videoPath}`);
|
|
}
|
|
|
|
const safeVideoPath = normalizePath(request.videoPath);
|
|
|
|
const outputDir = path.join(await Files.tempRoot(), `cover_previews_${Date.now()}`);
|
|
if (!fs.existsSync(outputDir)) {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
}
|
|
|
|
const safeOutputDir = normalizeOutputPath(outputDir);
|
|
|
|
const pythonScript = getPythonScriptPath('cover_preview_generator.py');
|
|
|
|
if (!fs.existsSync(pythonScript)) {
|
|
throw new Error(`Preview generator script not found: ${pythonScript}`);
|
|
}
|
|
|
|
const pythonCommand = `"${pythonScript}" "${safeVideoPath}" --output-dir "${safeOutputDir}"`;
|
|
Log.info(TAG, `Executing preview generation command`);
|
|
|
|
const result = await this.execPython(pythonCommand, 120000); // 2分钟超时
|
|
|
|
Log.info(TAG, `Preview generation completed`);
|
|
|
|
// 解析结果
|
|
let previewResult: any;
|
|
try {
|
|
const lines = result.trim().split('\n');
|
|
const jsonLine = lines[lines.length - 1];
|
|
previewResult = JSON.parse(jsonLine);
|
|
} catch (parseError) {
|
|
Log.error(TAG, `Failed to parse preview result: ${parseError}`);
|
|
throw new Error(`Invalid result format`);
|
|
}
|
|
|
|
if (previewResult.error) {
|
|
throw new Error(previewResult.error);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
previews: previewResult.previews || [],
|
|
message: `Generated ${previewResult.previews?.length || 0} previews`
|
|
};
|
|
|
|
} catch (error: any) {
|
|
Log.error(TAG, `Preview generation failed: ${error}`);
|
|
return {
|
|
success: false,
|
|
error: error.message || String(error),
|
|
message: `Preview generation failed: ${error.message || String(error)}`
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 使用自定义模板生成封面
|
|
* @param request 自定义模板生成请求
|
|
* @returns 生成结果
|
|
*/
|
|
async generateWithCustomTemplate(request: {
|
|
videoPath: string;
|
|
scriptText: string;
|
|
template: any; // CustomTemplate interface from frontend
|
|
}): Promise<GenerateResult> {
|
|
try {
|
|
Log.info(TAG, `Generating with custom template for video: ${request.videoPath}`);
|
|
|
|
// 验证输入参数
|
|
if (!request.videoPath) {
|
|
throw new Error("Video path is required");
|
|
}
|
|
|
|
if (!request.template) {
|
|
throw new Error("Custom template is required");
|
|
}
|
|
|
|
if (!fs.existsSync(request.videoPath)) {
|
|
throw new Error(`Video file not found: ${request.videoPath}`);
|
|
}
|
|
|
|
const safeVideoPath = normalizePath(request.videoPath);
|
|
|
|
const hubRoot = await Files.hubRoot();
|
|
Log.info(TAG, `Using hubRoot for output: ${hubRoot}`);
|
|
|
|
const outputDir = path.join(hubRoot, `subtitle_cover_${Date.now()}`);
|
|
if (!fs.existsSync(outputDir)) {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
}
|
|
|
|
const safeOutputDir = normalizeOutputPath(outputDir);
|
|
const safeHubRoot = normalizePath(hubRoot);
|
|
|
|
const config = {
|
|
videoPath: safeVideoPath,
|
|
scriptText: request.scriptText,
|
|
outputDir: safeOutputDir,
|
|
hubRoot: safeHubRoot,
|
|
customTemplate: true,
|
|
template: request.template
|
|
};
|
|
|
|
const configPath = await createTempConfig(config);
|
|
Log.info(TAG, `Created config file with custom template: ${configPath}`);
|
|
|
|
// 执行Python脚本
|
|
const pythonCommand = `"${this.getPythonScriptPath()}" "${configPath}"`;
|
|
Log.info(TAG, `Executing Python command with custom template`);
|
|
|
|
const result = await this.execPython(pythonCommand, 300000); // 5分钟超时
|
|
|
|
Log.info(TAG, `Python execution completed for custom template`);
|
|
|
|
// 解析结果
|
|
let generateResult: GenerateResult;
|
|
try {
|
|
// 查找JSON结果(可能在输出的最后)
|
|
const lines = result.trim().split('\n');
|
|
const jsonLine = lines[lines.length - 1];
|
|
generateResult = JSON.parse(jsonLine);
|
|
} catch (parseError) {
|
|
Log.error(TAG, `Failed to parse Python result: ${parseError}`);
|
|
throw new Error(`Invalid result format`);
|
|
}
|
|
|
|
// 清理临时配置文件
|
|
cleanupTempConfig(configPath);
|
|
|
|
Log.info(TAG, `Custom template generation completed: ${generateResult.message}`);
|
|
return generateResult;
|
|
|
|
} catch (error: any) {
|
|
Log.error(TAG, `Custom template generation failed: ${error}`);
|
|
return {
|
|
success: false,
|
|
error: error.message || String(error),
|
|
message: `Custom template generation failed: ${error.message || String(error)}`
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 检查Python环境和依赖
|
|
* @returns 检查结果
|
|
*/
|
|
async checkPythonEnvironment(): Promise<{
|
|
pythonAvailable: boolean;
|
|
scriptExists: boolean;
|
|
dependenciesInstalled: boolean;
|
|
missingLibraries?: string[];
|
|
message: string;
|
|
}> {
|
|
try {
|
|
// 检查Python是否可用
|
|
let pythonAvailable = false;
|
|
try {
|
|
await this.execPython('--version');
|
|
pythonAvailable = true;
|
|
} catch (e) {
|
|
pythonAvailable = false;
|
|
}
|
|
|
|
// 检查脚本文件是否存在
|
|
const scriptExists = fs.existsSync(this.getPythonScriptPath());
|
|
|
|
// 检查依赖库
|
|
const dependencies = ["cv2", "numpy", "PIL"];
|
|
const missingLibraries: string[] = [];
|
|
|
|
for (const lib of dependencies) {
|
|
try {
|
|
await this.execPython(`-c "import ${lib}"`);
|
|
} catch (e) {
|
|
missingLibraries.push(lib);
|
|
}
|
|
}
|
|
|
|
const dependenciesInstalled = missingLibraries.length === 0;
|
|
|
|
let message = "";
|
|
if (!pythonAvailable) {
|
|
message = "Python is not available";
|
|
} else if (!scriptExists) {
|
|
message = "Python script not found";
|
|
} else if (!dependenciesInstalled) {
|
|
message = `Missing dependencies: ${missingLibraries.join(", ")}`;
|
|
} else {
|
|
message = "Python environment is ready";
|
|
}
|
|
|
|
return {
|
|
pythonAvailable,
|
|
scriptExists,
|
|
dependenciesInstalled,
|
|
missingLibraries: missingLibraries.length > 0 ? missingLibraries : undefined,
|
|
message
|
|
};
|
|
|
|
} catch (error: any) {
|
|
Log.error(TAG, `Environment check failed: ${error}`);
|
|
return {
|
|
pythonAvailable: false,
|
|
scriptExists: false,
|
|
dependenciesInstalled: false,
|
|
message: `Environment check failed: ${error.message || String(error)}`
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取系统模板列表
|
|
*/
|
|
async getSystemTemplatesList(): Promise<{success: boolean; templates?: any[]; error?: string}> {
|
|
try {
|
|
|
|
// 从数据库获取系统模板
|
|
const subtitleTemplates = await DB.select(
|
|
`SELECT id, name, description, config, is_system, readonly, created_at, updated_at
|
|
FROM subtitle_templates WHERE is_system = 1 ORDER BY created_at ASC`
|
|
);
|
|
|
|
const coverTemplates = await DB.select(
|
|
`SELECT id, name, description, config, is_system, readonly, created_at, updated_at
|
|
FROM cover_templates WHERE is_system = 1 ORDER BY created_at ASC`
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
templates: {
|
|
subtitleTemplates: subtitleTemplates || [],
|
|
coverTemplates: coverTemplates || []
|
|
}
|
|
};
|
|
} catch (error: any) {
|
|
Log.error(TAG, `Get system templates list failed: ${error}`);
|
|
return {
|
|
success: false,
|
|
error: error.message || String(error)
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 保存/编辑系统模板
|
|
*/
|
|
async saveSystemTemplate(template: any): Promise<{success: boolean; id?: string; error?: string; message?: string}> {
|
|
try {
|
|
const now = Date.now();
|
|
|
|
if (template.type === 'subtitle') {
|
|
// 保存字幕模板
|
|
const configJson = JSON.stringify(template.config);
|
|
const existing = await DB.first(
|
|
`SELECT id FROM subtitle_templates WHERE id = ?`,
|
|
[template.id]
|
|
);
|
|
|
|
if (existing) {
|
|
await DB.execute(
|
|
`UPDATE subtitle_templates SET name = ?, description = ?, config = ?, updated_at = ? WHERE id = ?`,
|
|
[template.name, template.description, configJson, now, template.id]
|
|
);
|
|
} else {
|
|
await DB.execute(
|
|
`INSERT INTO subtitle_templates (id, name, description, config, is_system, readonly, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[template.id, template.name, template.description, configJson, 1, 0, now, now]
|
|
);
|
|
}
|
|
|
|
Log.info(TAG, `Saved subtitle template: ${template.id}`);
|
|
return {success: true, id: template.id, message: "Subtitle template saved successfully"};
|
|
|
|
} else if (template.type === 'cover') {
|
|
// 保存封面模板
|
|
const configJson = JSON.stringify(template.config);
|
|
const existing = await DB.first(
|
|
`SELECT id FROM cover_templates WHERE id = ?`,
|
|
[template.id]
|
|
);
|
|
|
|
if (existing) {
|
|
await DB.execute(
|
|
`UPDATE cover_templates SET name = ?, description = ?, config = ?, thumbnail_path = ?, updated_at = ? WHERE id = ?`,
|
|
[template.name, template.description, configJson, template.thumbnailPath || null, now, template.id]
|
|
);
|
|
} else {
|
|
await DB.execute(
|
|
`INSERT INTO cover_templates (id, name, config, is_system, readonly, created_at, updated_at, thumbnail_path)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[template.id, template.name, configJson, 1, 0, now, now, template.thumbnailPath || null]
|
|
);
|
|
}
|
|
|
|
Log.info(TAG, `Saved cover template: ${template.id}`);
|
|
return {success: true, id: template.id, message: "Cover template saved successfully"};
|
|
|
|
} else {
|
|
throw new Error("Invalid template type");
|
|
}
|
|
} catch (error: any) {
|
|
Log.error(TAG, `Save system template failed: ${error}`);
|
|
return {
|
|
success: false,
|
|
error: error.message || String(error)
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 删除系统模板
|
|
*/
|
|
async deleteSystemTemplate(templateId: string): Promise<{success: boolean; error?: string; message?: string}> {
|
|
try {
|
|
|
|
// 检查模板是否存在
|
|
const template = await DB.first(
|
|
`SELECT id, is_system FROM subtitle_templates WHERE id = ? UNION SELECT id, is_system FROM cover_templates WHERE id = ?`,
|
|
[templateId, templateId]
|
|
);
|
|
|
|
if (!template) {
|
|
throw new Error("Template not found");
|
|
}
|
|
|
|
if (template.is_system === 1) {
|
|
throw new Error("Cannot delete system templates");
|
|
}
|
|
|
|
// 从数据库删除
|
|
await DB.execute(`DELETE FROM subtitle_templates WHERE id = ?`, [templateId]);
|
|
await DB.execute(`DELETE FROM cover_templates WHERE id = ?`, [templateId]);
|
|
|
|
Log.info(TAG, `Deleted template: ${templateId}`);
|
|
return {success: true, message: "Template deleted successfully"};
|
|
|
|
} catch (error: any) {
|
|
Log.error(TAG, `Delete system template failed: ${error}`);
|
|
return {
|
|
success: false,
|
|
error: error.message || String(error)
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 重置系统模板为默认值
|
|
*/
|
|
async resetSystemTemplates(): Promise<{success: boolean; error?: string; message?: string}> {
|
|
try {
|
|
const { initSystemTemplates } = await import('../db/initSystemTemplates');
|
|
|
|
// 调用初始化函数重置系统模板
|
|
await initSystemTemplates();
|
|
|
|
Log.info(TAG, "System templates reset to defaults");
|
|
return {success: true, message: "System templates reset to defaults successfully"};
|
|
|
|
} catch (error: any) {
|
|
Log.error(TAG, `Reset system templates failed: ${error}`);
|
|
return {
|
|
success: false,
|
|
error: error.message || String(error)
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 保存字幕模板并自动导出(开发模式)
|
|
*/
|
|
async saveSubtitleTemplateAndExport(template: any, isDev: boolean = false): Promise<{success: boolean; id?: string; error?: string; message?: string}> {
|
|
try {
|
|
const now = Date.now();
|
|
|
|
const configJson = JSON.stringify(template.config);
|
|
const existing = await DB.first(
|
|
`SELECT id FROM subtitle_templates WHERE id = ?`,
|
|
[template.id]
|
|
);
|
|
|
|
if (existing) {
|
|
await DB.execute(
|
|
`UPDATE subtitle_templates SET name = ?, description = ?, config = ?, updated_at = ? WHERE id = ?`,
|
|
[template.name, template.description, configJson, now, template.id]
|
|
);
|
|
} else {
|
|
await DB.execute(
|
|
`INSERT INTO subtitle_templates (id, name, description, config, is_system, readonly, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[template.id, template.name, template.description, configJson, 0, 0, now, now]
|
|
);
|
|
}
|
|
|
|
// ✨ 关键:保存后自动导出到JSON(开发模式)
|
|
if (isDev) {
|
|
const exportResult = await this.exportSystemTemplatesToFile();
|
|
if (!exportResult.success) {
|
|
Log.warn(TAG, 'Failed to auto-export after saving', { templateId: template.id, error: exportResult.error });
|
|
// 不中断保存,只记录警告
|
|
} else {
|
|
Log.info(TAG, 'Auto-exported system template after saving', { templateId: template.id });
|
|
}
|
|
}
|
|
|
|
Log.info(TAG, `Saved subtitle template: ${template.id}`);
|
|
return { success: true, id: template.id, message: "Subtitle template saved successfully" };
|
|
|
|
} catch (error: any) {
|
|
Log.error(TAG, `Save subtitle template failed: ${error}`);
|
|
return {
|
|
success: false,
|
|
error: error.message || String(error)
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 保存封面模板并自动导出(开发模式)
|
|
*/
|
|
async saveCoverTemplateAndExport(template: any, isDev: boolean = false): Promise<{success: boolean; id?: string; error?: string; message?: string}> {
|
|
try {
|
|
const now = Date.now();
|
|
|
|
const configJson = JSON.stringify(template.config);
|
|
const existing = await DB.first(
|
|
`SELECT id FROM cover_templates WHERE id = ?`,
|
|
[template.id]
|
|
);
|
|
|
|
if (existing) {
|
|
await DB.execute(
|
|
`UPDATE cover_templates SET name = ?, description = ?, config = ?, thumbnail_path = ?, updated_at = ? WHERE id = ?`,
|
|
[template.name, template.description, configJson, template.thumbnailPath || null, now, template.id]
|
|
);
|
|
} else {
|
|
await DB.execute(
|
|
`INSERT INTO cover_templates (id, name, description, config, thumbnail_path, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
[template.id, template.name, template.description, configJson, template.thumbnailPath || null, now, now]
|
|
);
|
|
}
|
|
|
|
// ✨ 关键:保存后自动导出到JSON(开发模式)
|
|
if (isDev) {
|
|
const exportResult = await this.exportSystemTemplatesToFile();
|
|
if (!exportResult.success) {
|
|
Log.warn(TAG, 'Failed to auto-export after saving', { templateId: template.id, error: exportResult.error });
|
|
// 不中断保存,只记录警告
|
|
} else {
|
|
Log.info(TAG, 'Auto-exported system template after saving', { templateId: template.id });
|
|
}
|
|
}
|
|
|
|
Log.info(TAG, `Saved cover template: ${template.id}`);
|
|
return { success: true, id: template.id, message: "Cover template saved successfully" };
|
|
|
|
} catch (error: any) {
|
|
Log.error(TAG, `Save cover template failed: ${error}`);
|
|
return {
|
|
success: false,
|
|
error: error.message || String(error)
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 导出系统模板配置到 JSON 文件
|
|
*/
|
|
async exportSystemTemplatesToFile(): Promise<{success: boolean; filePath?: string; error?: string; message?: string}> {
|
|
try {
|
|
const pathModule = await import('path');
|
|
const fsModule = await import('fs');
|
|
|
|
// 从数据库获取所有系统模板
|
|
const subtitleTemplates = await DB.select(
|
|
`SELECT id, name, description, config, is_system, readonly, created_at, updated_at
|
|
FROM subtitle_templates WHERE is_system = 1 ORDER BY created_at ASC`
|
|
);
|
|
|
|
const coverTemplates = await DB.select(
|
|
`SELECT id, name, description, config, is_system, readonly, created_at, updated_at, thumbnail_path
|
|
FROM cover_templates WHERE is_system = 1 ORDER BY created_at ASC`
|
|
);
|
|
|
|
// 格式化数据
|
|
const subtitleTemplatesFormatted = (subtitleTemplates || []).map((t: any) => ({
|
|
id: t.id,
|
|
name: t.name,
|
|
description: t.description,
|
|
isSystem: Boolean(t.is_system),
|
|
readonly: Boolean(t.readonly),
|
|
createdAt: t.created_at,
|
|
config: typeof t.config === 'string' ? JSON.parse(t.config) : t.config
|
|
}));
|
|
|
|
const coverTemplatesFormatted = (coverTemplates || []).map((t: any) => ({
|
|
id: t.id,
|
|
name: t.name,
|
|
description: t.description,
|
|
is_system: t.is_system,
|
|
readonly: Boolean(t.readonly),
|
|
config: typeof t.config === 'string' ? JSON.parse(t.config) : t.config,
|
|
thumbnailPath: t.thumbnail_path,
|
|
createdAt: t.created_at
|
|
}));
|
|
|
|
// 生成配置对象
|
|
const systemTemplatesConfig = {
|
|
version: "1.0.0",
|
|
description: "系统内置模板配置 - 包含8套字幕模板和8套封面模板(开发模式可编辑,生产模式只读)",
|
|
timestamp: new Date().toISOString(),
|
|
subtitleTemplates: subtitleTemplatesFormatted,
|
|
coverTemplates: coverTemplatesFormatted
|
|
};
|
|
|
|
// 确定输出路径
|
|
const configDir = pathModule.join(__dirname, '../../config');
|
|
const configPath = pathModule.join(configDir, 'system-templates.json');
|
|
|
|
// 确保目录存在
|
|
if (!fsModule.existsSync(configDir)) {
|
|
fsModule.mkdirSync(configDir, { recursive: true });
|
|
}
|
|
|
|
// 保存到文件
|
|
fsModule.writeFileSync(
|
|
configPath,
|
|
JSON.stringify(systemTemplatesConfig, null, 2),
|
|
'utf-8'
|
|
);
|
|
|
|
Log.info(TAG, `Exported system templates to ${configPath}`, {
|
|
subtitleCount: subtitleTemplatesFormatted.length,
|
|
coverCount: coverTemplatesFormatted.length,
|
|
filePath: configPath
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
filePath: configPath,
|
|
message: `Successfully exported ${subtitleTemplatesFormatted.length} subtitle templates and ${coverTemplatesFormatted.length} cover templates`
|
|
};
|
|
|
|
} catch (error: any) {
|
|
Log.error(TAG, `Export system templates failed: ${error}`);
|
|
return {
|
|
success: false,
|
|
error: error.message || String(error)
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
export default SubtitleCoverGenerator;
|