From 25073109859ae09a6c1a3999c68d32c09e51c919 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Thu, 31 Jul 2025 22:55:10 +0200 Subject: [PATCH] Add new file name template variables `CUT_DURATION`, `CUT_FROM_NUM`, `CUT_TO_NUM` also don't show advanced variable in simple mode closes #2486 --- docs.md | 5 +++- .../src/components/FileNameTemplateEditor.tsx | 18 ++++++++++--- src/renderer/src/util/outputNameTemplate.ts | 25 +++++++++++++------ 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/docs.md b/docs.md index 9879880f..306d19ad 100644 --- a/docs.md +++ b/docs.md @@ -67,14 +67,17 @@ The following variables are available in the template to customize the filenames | ✅ | `${EPOCH_MS}` | `number` | Number of milliseconds since epoch (e.g. `1680852771465`). Useful to generate a unique file name on every export to prevent accidental overwrite. | ✅ | `${EXPORT_COUNT}` | `number` | Number of exports done since last LosslessCut launch (starts at 1). | ✅ | `${FILE_EXPORT_COUNT}` | `number` | Number of exports done since last file was opened (starts at 1). -| ✅ | `${SEG_LABEL}` | `string` | The label of the segment (e.g. `Getting Lunch`). In cut+merge mode, this will be an `Array`, and you can use e.g. this code to combine all labels with a comma between: `${SEG_LABEL.filter(label => label).join(',')}` +| ✅ | `${SEG_LABEL}` | `string` / `string[]` | The label of the segment (e.g. `Getting Lunch`). In cut+merge mode, this will be an `Array`, and you can use e.g. this code to combine all labels with a comma between: `${SEG_LABEL.filter(label => label).join(',')}` | | `${SEG_NUM}` | `string` | Segment index, padded string (e.g. `01`, `02` or `42`). | | `${SEG_NUM_INT}` | `number` | Segment index, as an integer (e.g. `1`, `2` or `42`). Can be used with numeric arithmetics, e.g. `${SEG_NUM_INT+100}`. | | `${SELECTED_SEG_NUM}` | `string` | Same as `SEG_NUM`, but it counts only selected segments. | | `${SELECTED_SEG_NUM_INT}` | `number` | Same as `SEG_NUM_INT`, but it counts only selected segments. | | `${SEG_SUFFIX}` | `string` | If a label exists for this segment, the label will be used, prepended by `-`. Otherwise, the segment index prepended by `-seg` will be used (e.g. `-Getting_Lunch`, `-seg1`). | | `${CUT_FROM}` | `string` | The timestamp for the beginning of the segment in `hh.mm.ss.sss` format (e.g. `00.00.27.184`). +| | `${CUT_FROM_NUM}` | `number` | Same as `${CUT_FROM}`, but numeric, meaning it can be used with arithmetics. | | `${CUT_TO}` | `string` | The timestamp for the ending of the segment in `hh.mm.ss.sss` format (e.g. `00.00.28.000`). +| | `${CUT_TO_NUM}` | `number` | See `${CUT_FROM_NUM}`. +| | `${CUT_DURATION}` | `string` | The duration of the segment (`CUT_TO-CUT_FROM`) in `hh.mm.ss.sss` format (e.g. `00.00.28.000`). | | `${SEG_TAGS.XX}` | `object` | Allows you to retrieve the tags for a given segment by name. If a tag is called foo, it can be accessed with `${SEG_TAGS.foo}`. Note that if the tag does not exist, it will yield the text `undefined`. You can work around this as follows: `${SEG_TAGS.foo ?? ''}` Your files must always include at least one unique identifer (such as `${SEG_NUM}` or `${CUT_FROM}`), and it should end in `${EXT}` (or else players might not recognise the files). For instance, to achieve a filename sequence of `Beach Trip - 1.mp4`, `Beach Trip - 2.mp4`, `Beach Trip - 3.mp4`, your format should read `${FILENAME} - ${SEG_NUM}${EXT}`. If your template gives at least two duplicate output file names, LosslessCut will revert to using the default template instead. diff --git a/src/renderer/src/components/FileNameTemplateEditor.tsx b/src/renderer/src/components/FileNameTemplateEditor.tsx index 7fd0a684..c2f864ee 100644 --- a/src/renderer/src/components/FileNameTemplateEditor.tsx +++ b/src/renderer/src/components/FileNameTemplateEditor.tsx @@ -36,7 +36,7 @@ function FileNameTemplateEditor(opts: { })) { const { template: templateIn, setTemplate, defaultTemplate, generateFileNames, mergeMode } = opts; - const { safeOutputFileName, toggleSafeOutputFileName, outputFileNameMinZeroPadding, setOutputFileNameMinZeroPadding } = useUserSettings(); + const { safeOutputFileName, toggleSafeOutputFileName, outputFileNameMinZeroPadding, setOutputFileNameMinZeroPadding, simpleMode } = useUserSettings(); const [text, setText] = useState(templateIn); const [debouncedText] = useDebounce(text, 500); @@ -78,8 +78,20 @@ function FileNameTemplateEditor(opts: { const availableVariables = useMemo(() => (mergeMode ? ['FILENAME', extVariable, 'EPOCH_MS', 'EXPORT_COUNT', 'FILE_EXPORT_COUNT', 'SEG_LABEL'] - : ['FILENAME', extVariable, 'EPOCH_MS', 'EXPORT_COUNT', 'FILE_EXPORT_COUNT', 'SEG_LABEL', 'CUT_FROM', 'CUT_TO', segNumVariable, segNumIntVariable, selectedSegNumVariable, selectedSegNumIntVariable, segSuffixVariable, segTagsExample] - ), [mergeMode]); + : [ + 'FILENAME', extVariable, 'EPOCH_MS', 'EXPORT_COUNT', 'FILE_EXPORT_COUNT', 'SEG_LABEL', + 'CUT_FROM', + ...(!simpleMode ? ['CUT_FROM_NUM'] : []), + 'CUT_TO', + ...(!simpleMode ? ['CUT_TO_NUM'] : []), + 'CUT_DURATION', + segNumVariable, + ...(!simpleMode ? [segNumIntVariable] : []), + selectedSegNumVariable, + ...(!simpleMode ? [selectedSegNumIntVariable] : []), + segSuffixVariable, segTagsExample, + ] + ), [mergeMode, simpleMode]); // eslint-disable-next-line no-template-curly-in-string const isMissingExtension = validText != null && !validText.endsWith(extVariableFormatted); diff --git a/src/renderer/src/util/outputNameTemplate.ts b/src/renderer/src/util/outputNameTemplate.ts index b7d3542c..9d6ed2af 100644 --- a/src/renderer/src/util/outputNameTemplate.ts +++ b/src/renderer/src/util/outputNameTemplate.ts @@ -115,7 +115,7 @@ export const defaultCutMergedFileTemplate = '${FILENAME}-cut-merged-${EPOCH_MS}$ export const defaultMergedFileTemplate = '${FILENAME}-merged-${EPOCH_MS}${EXT}'; -async function interpolateOutFileName(template: string, { epochMs, inputFileNameWithoutExt, ext, segSuffix, segNum, segNumPadded, selectedSegNum, selectedSegNumPadded, segLabels, cutFrom, cutTo, tags, exportCount, currentFileExportCount }: { +async function interpolateOutFileName(template: string, { epochMs, inputFileNameWithoutExt, ext, segSuffix, segNum, segNumPadded, selectedSegNum, selectedSegNumPadded, segLabels, cutFrom, cutFromStr, cutTo, cutToStr, cutDurationStr, tags, exportCount, currentFileExportCount }: { epochMs: number, inputFileNameWithoutExt: string, ext: string, @@ -128,8 +128,11 @@ async function interpolateOutFileName(template: string, { epochMs, inputFileName segNumPadded: string, selectedSegNum: number, selectedSegNumPadded: string, - cutFrom: string, - cutTo: string, + cutFrom: number, + cutFromStr: string, + cutTo: number, + cutToStr: string, + cutDurationStr: string, tags: Record, }>) { const context = { @@ -142,8 +145,11 @@ async function interpolateOutFileName(template: string, { epochMs, inputFileName [selectedSegNumVariable]: selectedSegNumPadded, SEG_LABEL: segLabels.length === 1 ? segLabels[0] : segLabels, EPOCH_MS: epochMs, - CUT_FROM: cutFrom, - CUT_TO: cutTo, + CUT_FROM: cutFromStr, + CUT_FROM_NUM: cutFrom, + CUT_TO: cutToStr, + CUT_TO_NUM: cutTo, + CUT_DURATION: cutDurationStr, [segTagsVariable]: tags && { // allow both original case and uppercase ...tags, @@ -239,6 +245,8 @@ export async function generateOutSegFileNames({ fileDuration, segmentsToExport: const { name: inputFileNameWithoutExt } = parsePath(filePath); + const cutDuration = end - start; + const segFileName = await interpolateOutFileName(template, { epochMs, segNum, @@ -249,8 +257,11 @@ export async function generateOutSegFileNames({ fileDuration, segmentsToExport: inputFileNameWithoutExt, ext: getOutFileExtension({ isCustomFormatSelected, outFormat: fileFormat, filePath }), segLabels: [sanitizeName(name)], - cutFrom: formatTimecode({ seconds: start, fileNameFriendly: true }), - cutTo: formatTimecode({ seconds: end, fileNameFriendly: true }), + cutFrom: start, + cutFromStr: formatTimecode({ seconds: start, fileNameFriendly: true }), + cutTo: end, + cutToStr: formatTimecode({ seconds: end, fileNameFriendly: true }), + cutDurationStr: formatTimecode({ seconds: cutDuration, fileNameFriendly: true }), tags: Object.fromEntries(Object.entries(getSegmentTags(segment)).map(([tag, value]) => [tag, sanitizeName(value)])), exportCount, currentFileExportCount,