const fs = require("fs"); const path = require("path"); const IMAGE_EXT = new Set([".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"]); function isImageFile(filePath) { return IMAGE_EXT.has(path.extname(filePath).toLowerCase()); } function listMediaFiles(folderPath) { if (!folderPath || !fs.existsSync(folderPath)) return []; const entries = fs.readdirSync(folderPath, { withFileTypes: true }); const files = []; for (const entry of entries) { if (!entry.isFile()) continue; const ext = path.extname(entry.name).toLowerCase(); if ( [".mp4", ".mov", ".webm", ".mkv", ".m4v", ".avi", ...IMAGE_EXT].includes(ext) ) { files.push(path.join(folderPath, entry.name)); } } return files.sort(); } function pickFromFolder(selectionMode, folderPath, specifiedPath) { if (selectionMode === "specified" && specifiedPath) { return specifiedPath; } const files = listMediaFiles(folderPath); if (!files.length) return null; if (selectionMode === "sequential") { return files[0]; } return files[Math.floor(Math.random() * files.length)]; } function buildSubtitleModeReplacements(records, subtitleModeConfig, probeVideoDuration) { const mappings = Array.isArray(subtitleModeConfig?.mappings) ? subtitleModeConfig.mappings : []; const replacements = []; for (const mapping of mappings) { if (!mapping?.materialPath || !fs.existsSync(mapping.materialPath)) continue; const indices = Array.isArray(mapping.subtitleIndices) ? mapping.subtitleIndices : []; if (!indices.length || !records.length) continue; let minStart = Infinity; let maxEnd = -Infinity; for (const idx of indices) { const record = records[idx]; if (!record) continue; const startSec = Number(record.start ?? record.startTime ?? 0) / 1000; const endSec = Number(record.end ?? record.endTime ?? 0) / 1000; minStart = Math.min(minStart, startSec); maxEnd = Math.max(maxEnd, endSec); } if (!Number.isFinite(minStart) || !Number.isFinite(maxEnd)) continue; const duration = maxEnd - minStart; if (duration <= 0) continue; const isImage = mapping.isImage ?? isImageFile(mapping.materialPath); const materialDuration = isImage ? duration : probeVideoDuration(mapping.materialPath); replacements.push({ startTime: minStart, duration, materialPath: mapping.materialPath, materialDuration, materialStartOffset: 0, displayMode: mapping.displayMode || "fullscreen", pipPosition: mapping.pipPosition || "bottom-right", pipSizePercent: mapping.pipSizePercent ?? 30, pipScaleMode: mapping.pipScaleMode || "fit", isImage, originalVideoMinimized: mapping.originalVideoMinimized, }); } return replacements.sort((a, b) => a.startTime - b.startTime); } function buildFixedModeReplacements(fixedModeConfig, videoDuration, probeVideoDuration) { const replacements = []; if (!fixedModeConfig?.enabled) return replacements; const segments = [ { key: "opening", reason: "片头" }, { key: "middle", reason: "片中" }, { key: "ending", reason: "片尾" }, ]; for (const { key, reason } of segments) { const seg = fixedModeConfig[key]; if (!seg?.enabled) continue; const min = Number(seg.timeRange?.durationMin ?? 2); const max = Number(seg.timeRange?.durationMax ?? 5); let startTime = 0; let duration = max - min; if (key === "middle") { const mid = videoDuration / 2; startTime = Math.max(0, mid + min); duration = Math.min(videoDuration, mid + max) - startTime; } else if (key === "ending") { duration = max - min; startTime = Math.max(0, videoDuration - duration); } else { startTime = min; duration = max - min; } if (duration <= 0 || startTime >= videoDuration) continue; const materialPath = pickFromFolder( seg.selectionMode || "random", seg.materialFolderPath, seg.specifiedMaterialPath, ); if (!materialPath || !fs.existsSync(materialPath)) continue; const isImage = isImageFile(materialPath); replacements.push({ startTime, duration, materialPath, materialDuration: isImage ? duration : probeVideoDuration(materialPath), materialStartOffset: 0, displayMode: seg.displayMode || "fullscreen", pipPosition: seg.pipPosition || "bottom-right", pipSizePercent: seg.pipSizePercent ?? 30, pipScaleMode: seg.pipScaleMode || "fit", isImage, originalVideoMinimized: seg.originalVideoMinimized, reason, }); } return replacements.sort((a, b) => a.startTime - b.startTime); } function buildReplacementsFromMixCutSettings(settings, videoDuration, probeVideoDuration) { if (!settings || typeof settings !== "object" || !settings.enabled) { return []; } const mode = settings.mode || "subtitle"; let replacements = []; if (mode === "subtitle") { const records = Array.isArray(settings.records) ? settings.records : []; replacements = buildSubtitleModeReplacements( records, settings.subtitleModeConfig, probeVideoDuration, ); } else if (mode === "fixed" && settings.fixedModeConfig?.enabled) { replacements = buildFixedModeReplacements( settings.fixedModeConfig, videoDuration, probeVideoDuration, ); } return replacements; } module.exports = { buildReplacementsFromMixCutSettings, buildSubtitleModeReplacements, buildFixedModeReplacements, };