LosslessCut 导出文件名模板完整指南:变量、JavaScript 表达式与实战配置
【免费下载链接】lossless-cutThe swiss army knife of lossless video/audio editing项目地址: https://gitcode.com/gh_mirrors/lo/lossless-cut
LosslessCut 在导出片段(segments)为文件时,允许用户通过**文件名模板(file name template)**精确控制输出文件的命名规则。本文基于 docs/file-name-template.md 系统讲解模板的全部内置变量、JavaScript 表达式能力、数字补零技巧与回退机制,并结合 src/renderer/src/util/outputNameTemplate.ts 等源码剖析其底层实现。读完本文,你将能编写出可复制的模板,实现「Beach Trip - 1.mp4」式的序列命名、带时间码的命名、基于标签的命名以及防覆盖的唯一命名。
一、模板是什么:JavaScript 模板字符串
当导出片段为文件时,LosslessCut 允许你通过一个**模板(template)**来规定输出文件如何按顺序命名。模板本质上是一个 JavaScript 模板字符串(template literal),也就是说,你可以在模板内部直接使用 JavaScript 语法,例如${...}插值、方法调用、三元运算、数组方法等。
从实现上看,模板字符串在运行时会被构造为`...`代码,并交由一个独立的 Web Worker 执行。入口位于 src/renderer/src/worker/eval.ts 的safeishEval函数:它把代码与序列化后的上下文一起 postMessage 给 Worker,Worker 端(src/renderer/src/worker/evalWorker.ts)通过Function('\nwith (this) { return (${code}); }').call(context)在受限的with作用域中求值。为了安全,Worker 对全局对象做了白名单隔离,只暴露Array、Date、Math、String、JSON、RegExp等基础对象,其余属性一律抛出Security Exception,因此你无法在模板中访问文件系统或网络。
所有可用变量的类型定义(TypeScript 接口)位于 src/common/userTypes.ts 的FileNameTemplateContext,其 JSDoc 注释会同步生成到 docs/generated/types.md,是理解每个变量类型与可选性的权威依据。模板中这些变量之所以可用,是因为 interpolateOutFileName 在求值前构建了一个包含上述全部变量的上下文对象。
二、全部内置变量速查表
下表完整列出了模板中可用的变量。其中「合并文件」指 Merge 模式(将多个文件合并导出为一个文件),「剪切+合并」指 Cut + Merge 模式(将多个片段合并导出一个文件);带 ✅ 表示该模式下可用。
| 合并文件可用 | 剪切+合并可用 | 变量 | 类型 | 输出说明 |
|---|---|---|---|---|
| ✅ | ✅ | ${FILENAME} | string | 原始文件名不含扩展名(例如文件Beach Trip.mp4得到Beach Trip)。合并文件时取第一个原始文件名。 |
| ✅ | ✅ | ${FILES} | SourceFile[] | 🤓🧪 原始文件对象数组,每个元素是SourceFile,可用 JavaScript 表达式查询属性。示例:${new Date(FILES[0].ctime).toISOString().replaceAll(':', '.').replaceAll('T', ' ')} |
| ✅ | ✅ | ${EXT} | string | 文件扩展名(如.mp4、.mkv)。 |
| ✅ | ✅ | ${EPOCH_MS} | number | 自 Unix 纪元起的毫秒数(如1680852771465),每次导出生成唯一名,可防止意外覆盖。 |
| ✅ | ✅ | ${EXPORT_COUNT} | number | 自本次 LosslessCut 启动以来完成的导出次数(从 1 开始)。 |
| ✅ | ${FILE_EXPORT_COUNT} | number | 自当前文件打开以来完成的导出次数(从 1 开始)。 | |
| ✅ | ✅ | ${SEG_LABEL} | string/string[] | 片段的标签(如Getting Lunch)。Cut + Merge 模式下为数组,可用${SEG_LABEL.filter(label => label).join(',')}合并所有标签;合并文件时是每个被合并原始文件的名称。 |
${SEG_NUM} | string | 片段序号,零填充字符串(如01、02或42)。 | ||
${SEG_NUM_INT} | number | 🤓 片段序号整数(如1、2或42),可用于数值运算,如${SEG_NUM_INT+100}。 | ||
${SELECTED_SEG_NUM} | string | 同SEG_NUM,但仅对选中的片段计数。 | ||
${SELECTED_SEG_NUM_INT} | number | 🤓 同SEG_NUM_INT,但仅对选中的片段计数。 | ||
${SEG_SUFFIX} | string | 若该片段有标签,则用标签并前置-;否则用序号前置-seg(例如-Getting_Lunch、-seg1)。 | ||
${CUT_FROM} | string | 片段起始时间戳,格式hh.mm.ss.sss(如00.00.27.184)。 | ||
${CUT_FROM_NUM} | number | 🤓 同${CUT_FROM},但为数值,可用于算术运算。 | ||
${CUT_TO} | string | 片段结束时间戳,格式hh.mm.ss.sss(如00.00.28.000)。 | ||
${CUT_TO_NUM} | number | 🤓 同${CUT_FROM_NUM}。 | ||
${CUT_DURATION} | string | 片段时长(CUT_TO - CUT_FROM),格式hh.mm.ss.sss(如00.00.28.000)。 | ||
${SEG_TAGS.XX} | object | 按名称取片段的标签。标签名为 foo 时写作${SEG_TAGS.foo};标签不存在时输出文本undefined,可用${SEG_TAGS.foo ?? ''}规避。 |
🤓 = 高级变量(进阶用户),涉及 JavaScript 表达式;🧪 = 实验性功能。
值得注意的实现细节:SEG_TAGS在上下文中同时保留了原始大小写和大写两种键名(见 interpolateOutFileName 中Object.fromEntries的处理),因此${SEG_TAGS.foo}与${SEG_TAGS.FOO}均可命中,容错性更好。而标签值在进入模板前会经过净化处理,确保不会引入目标文件系统不支持的字符。
三、变量可用性与默认模板
不同导出模式下变量的可用范围并不相同。从上表可以看出:SEG_NUM、CUT_FROM、SEG_TAGS等片段级变量只在普通剪切导出(Cut,逐段导出为独立文件)时可用;合并与剪切+合并模式更依赖FILENAME、FILES、EXT、EPOCH_MS、EXPORT_COUNT、SEG_LABEL这类文件级变量。
模板必须满足两个硬性要求:
- 至少包含一个唯一标识符,例如
${SEG_NUM}或${CUT_FROM},否则多个输出文件可能重名; - 应以
${EXT}结尾,否则播放器可能无法识别文件类型。
若模板生成的文件名至少出现两个重复,LosslessCut 会自动回退到默认模板。源码中定义了三个默认模板(见 outputNameTemplate.ts):
// 普通剪切导出(逐段导出为独立文件) export const defaultCutFileTemplate = '${FILENAME}-${CUT_FROM}-${CUT_TO}${SEG_SUFFIX}${EXT}'; // 剪切+合并导出 export const defaultCutMergedFileTemplate = '${FILENAME}-cut-merged-${EPOCH_MS}${EXT}'; // 合并导出 export const defaultMergedFileTemplate = '${FILENAME}-merged-${EPOCH_MS}${EXT}';注意默认模板刻意加入了CUT_FROM、CUT_TO、EPOCH_MS等保证唯一性的元素,注释也写明"fallback 模板必须始终生成唯一文件名"。回退逻辑位于generateWithFallback(outputNameTemplate.ts):当自定义模板求值抛错或getTemplateProblems检测到重复命名、非法字符等问题时,如果自定义模板与默认模板不同,就改用默认模板重试,并返回originalFileNames与problems供界面提示。
典型示例:要实现Beach Trip - 1.mp4、Beach Trip - 2.mp4、Beach Trip - 3.mp4这样的序列命名,模板应写为:
${FILENAME} - ${SEG_NUM}${EXT}四、模板校验规则:什么样的模板会被拒绝
在文件名生成后、写盘之前,getTemplateProblems(outputNameTemplate.ts)会对每个结果逐一校验,任一文件不满足即触发回退。校验项包括:
- 非法字符:Windows 下禁止
< > : " | ? *;macOS 下禁止:;启用安全文件名(safeOutputFileName)时还会把路径分隔符加入黑名单(Windows 为/与\,其他平台为系统分隔符),防止模板注入目录结构。 - 与输入路径相同:生成路径与源文件路径一致时拒绝,避免覆盖原文件。
- 结尾字符:Windows 下文件名不允许以空格或点结尾。
- 路径过长:Windows 或开发模式下,完整输出路径达到 259 字符即拒绝(Windows 最大路径限制为 256/259 字符,macOS 为 255)。
- 空文件名:结果为空字符串时报错。
- 重复文件名:多个输出同名时拒绝,并提示可加入
${SEG_NUM}修复。
此外,模板中的标签、后缀等"非来自源文件名的部分"会先经过filenamify净化(src/renderer/src/util.ts):将不属于 Unicode 字母、数字、空格、点、下划线、连字符的字符替换为_。片段级文件名还会被截断到 250 字符(maxFileNameLength)以内,见maybeTruncatePath。
五、数字补零(Padding)
如果需要对数字进行补零,可以在变量外包裹 JavaScript 代码。例如将FILE_EXPORT_COUNT补零到 2 位:先把数字转为String,再调用字符串的padStart:
${String(FILE_EXPORT_COUNT).padStart(2, '0')}其原理与SEG_NUM的实现一致——源码中的formatSegNum(src/renderer/src/segments.ts)正是通过`${segIndex + 1}`.padStart(Math.max(numDigits, minLength), '0')来生成零填充序号:SEG_NUM的填充宽度由全部片段数决定(片段 9 的两位数位数),也可以进一步由设置中的最小零填充位数控制。因此当你需要SEG_NUM固定 3 位时,可以自行用padStart(3, '0')包装(注意SEG_NUM本身已是字符串,无需再String())。
如果需要更多补零技巧,可以求助于 AI,例如提问:"How to pad a number with JavaScript?"(如何用 JavaScript 给数字补零?)。
六、实战模板示例合集
结合变量表与源码行为,下面给出可直接粘贴使用的模板:
1. 序号命名(最常用,保证唯一)
${FILENAME} - ${SEG_NUM}${EXT}2. 片段时间范围命名(默认模板风格,适合想从文件名看出剪切点)
${FILENAME}-${CUT_FROM}-${CUT_TO}${SEG_SUFFIX}${EXT}3. 带标签的命名(片段有标签时显示标签,无标签时退化为-segN)
${FILENAME}${SEG_SUFFIX}${EXT}4. 防覆盖唯一命名(每次导出时间戳不同,适合反复导出)
${FILENAME}-${EPOCH_MS}${EXT}5. 标签合并(Cut + Merge 模式)
${FILENAME}-${SEG_LABEL.filter(label => label).join(',')}${EXT}6. 数值运算与时间码
${FILENAME}-seg${SEG_NUM_INT+100}-from${CUT_FROM}${EXT}7. 缺失标签安全处理
${FILENAME}${SEG_SUFFIX}-${SEG_TAGS.scene ?? ''}${EXT}8. 基于源文件属性的高级用法(🤓🧪):例如把文件的创建时间转成可读日期并替换掉:与T字符,使其成为合法文件名:
${new Date(FILES[0].ctime).toISOString().replaceAll(':', '.').replaceAll('T', ' ')} ${FILENAME}${EXT}需要说明的是:FILES[0]的各时间属性(atime、mtime、ctime、birthtime)单位是毫秒级时间戳(见 SourceFile 定义),size为字节数;该功能标记为实验性,跨平台时间字段行为可能略有差异。
七、底层执行链路与注意事项
一次模板求值的完整链路为:界面读取用户模板 →generateCutFileNames/generateCutMergedFileNames/generateMergedFileNames(对应三种导出模式)组装上下文并调用interpolateOutFileName→ 构造`模板`代码交给safeishEval→ Worker 沙箱内with (context)求值返回字符串 →getTemplateProblems校验 → 出错则回退默认模板。这解释了为什么模板必须"以 JavaScript 模板字符串语法书写",也解释了为什么表达式中能使用Array、Date、Math、String等内置对象。
实际使用时请注意:
- 模板求值结果必须是字符串,否则会抛出
Expression did not lead to a string(见 outputNameTemplate.ts)。 - 语法错误、非法字符、重复文件名都会触发回退到默认模板,界面会同时给出错误原因提示。
- 由于求值在受限 Worker 中进行,不要尝试访问
window、document、fetch或任意未列入白名单的全局对象,它们会抛出Security Exception。 - 标签名大小写均可访问,但请记得用
?? ''处理缺失标签,避免文件名中出现字面量undefined。
掌握上述变量与规则后,你可以在 LosslessCut 的导出设置中直接粘贴模板,实现完全自主控制的批量输出命名。若希望获得更具体的模板写法,也可以把本文档作为提示词交给 AI,让它帮你按需求生成模板字符串。
【免费下载链接】lossless-cutThe swiss army knife of lossless video/audio editing项目地址: https://gitcode.com/gh_mirrors/lo/lossless-cut
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考