From d84f31fd721c10f4ddbe5325b53e6d5051c9585d Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 15 Mar 2025 14:52:09 +0800 Subject: [PATCH] change SEG_NUM and SESG_NUM_INT variables they will now correspond to the original index of the segment as shown in the number on the segment list new variables SELECTED_SEG_NUM and SELECTED_SEG_NUM_INT can be used for previous behavior (where the numbering is based on the segment's number in the list of *selected* segments). also minor change: most operations will no longer take place if no segments selected closes #2335 --- docs.md | 2 + src/renderer/src/App.tsx | 22 +++++------ src/renderer/src/SegmentList.tsx | 1 - .../src/components/FileNameTemplateEditor.tsx | 8 ++-- src/renderer/src/hooks/useFfmpegOperations.ts | 4 +- src/renderer/src/hooks/useSegments.ts | 28 +++++++------- src/renderer/src/segments.ts | 4 +- src/renderer/src/types.ts | 1 + src/renderer/src/util/outputNameTemplate.ts | 37 +++++++++++++------ 9 files changed, 63 insertions(+), 44 deletions(-) diff --git a/docs.md b/docs.md index 75b38b30..bac12a17 100644 --- a/docs.md +++ b/docs.md @@ -69,6 +69,8 @@ The following variables are available in the template to customize the filenames | | `${FILE_EXPORT_COUNT}` | `number` | Number of exports done since last file was opened (starts at 1). | | `${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_LABEL}` | `string` | The label of the segment (e.g. `Getting Lunch`). | | `${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`). diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 89de557f..6fb75693 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -88,7 +88,7 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti import BigWaveform from './components/BigWaveform'; import isDev from './isDev'; -import { BatchFile, Chapter, CustomTagsByFile, EdlExportType, EdlFileType, EdlImportType, FfmpegCommandLog, FilesMeta, goToTimecodeDirectArgsSchema, openFilesActionArgsSchema, ParamsByStreamId, PlaybackMode, SegmentBase, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, TunerType } from './types'; +import { BatchFile, Chapter, CustomTagsByFile, EdlExportType, EdlFileType, EdlImportType, FfmpegCommandLog, FilesMeta, goToTimecodeDirectArgsSchema, openFilesActionArgsSchema, ParamsByStreamId, PlaybackMode, SegmentBase, SegmentColorIndex, SegmentTags, StateSegment, TunerType } from './types'; import { CaptureFormat, KeyboardAction, Html5ifyMode, WaveformMode, ApiActionRequest, KeyBinding } from '../../../types'; import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe'; import useLoading from './hooks/useLoading'; @@ -767,7 +767,7 @@ function App() { if (playbackModeRef.current === 'loop-selected-segments') { const firstSelectedSegment = selectedSegments[0]; if (firstSelectedSegment != null) { - const index = cutSegments.indexOf(firstSelectedSegment); + const index = cutSegments.findIndex((segment) => segment.segId === firstSelectedSegment.segId); if (index >= 0) setCurrentSegIndex(index); seekAbs(firstSelectedSegment.start); } @@ -797,7 +797,7 @@ function App() { if (nextAction != null) { console.log(nextAction); if (nextAction.nextSegment) { - const index = selectedSegments.indexOf(playingSegment); + const index = selectedSegments.findIndex((selectedSegment) => selectedSegment.segId === playingSegment.segId); let newIndex = getNewJumpIndex(index >= 0 ? index : 0, 1); if (newIndex > selectedSegments.length - 1) newIndex = 0; // have reached end of last segment, start over const nextSelectedSegment = selectedSegments[newIndex]; @@ -1021,12 +1021,12 @@ function App() { } }, [cleanupFilesWithDialog, isFileOpened, setWorking, workingRef]); - const generateOutSegFileNames = useCallback(async ({ segments = segmentsToExport, template }: { segments?: SegmentToExport[], template: string }) => { + const generateOutSegFileNames = useCallback(async (template: string) => { invariant(fileFormat != null && outputDir != null && filePath != null); - return generateOutSegFileNamesRaw({ fileDuration, exportCount, currentFileExportCount, segments, template, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding }); + return generateOutSegFileNamesRaw({ fileDuration, exportCount, currentFileExportCount, segmentsToExport, template, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding }); }, [currentFileExportCount, exportCount, fileDuration, fileFormat, filePath, formatTimecode, isCustomFormatSelected, maxLabelLength, outputDir, outputFileNameMinZeroPadding, safeOutputFileName, segmentsToExport]); - const generateMergedFileNames = useCallback(async ({ template }: { template: string }) => { + const generateMergedFileNames = useCallback(async (template: string) => { invariant(fileFormat != null && filePath != null); return generateMergedFileNamesRaw({ template, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, exportCount, currentFileExportCount }); }, [currentFileExportCount, exportCount, fileFormat, filePath, isCustomFormatSelected, outputDir, safeOutputFileName]); @@ -1071,7 +1071,7 @@ function App() { const notices: string[] = []; const warnings: string[] = []; - const { fileNames: outSegFileNames, problems: outSegProblems } = await generateOutSegFileNames({ segments: segmentsToExport, template: outSegTemplateOrDefault }); + const { fileNames: outSegFileNames, problems: outSegProblems } = await generateOutSegFileNames(outSegTemplateOrDefault); if (outSegProblems.error != null) { console.warn('Output segments file name invalid, using default instead', outSegFileNames); warnings.push(t('Fell back to default output file name'), outSegProblems.error); @@ -1114,7 +1114,7 @@ function App() { const chapterNames = segmentsToChapters && !invertCutSegments ? segmentsToExport.map((s) => s.name) : undefined; - const { fileNames, problems } = await generateMergedFileNames({ template: mergedFileTemplateOrDefault }); + const { fileNames, problems } = await generateMergedFileNames(mergedFileTemplateOrDefault); if (problems.error != null) { console.warn('Merged file name invalid, using default instead', fileNames[0]); warnings.push(t('Fell back to default output file name'), problems.error); @@ -1234,7 +1234,7 @@ function App() { }, [filePath, getRelevantTime, videoRef, usingPreviewFile, captureFrameMethod, captureFrameFromFfmpeg, customOutDir, captureFormat, captureFrameQuality, captureFrameFromTag, hideAllNotifications]); const extractSegmentsFramesAsImages = useCallback(async (segments: SegmentBase[]) => { - if (!filePath || detectedFps == null || workingRef.current) return; + if (!filePath || detectedFps == null || workingRef.current || segments.length === 0) return; const segmentsNumFrames = segments.reduce((acc, { start, end }) => acc + (end == null ? 1 : (getFrameCount(end - start) ?? 0)), 0); const areAllSegmentsMarkers = segments.every((seg) => seg.end == null); const captureFramesResponse = areAllSegmentsMarkers @@ -1901,7 +1901,7 @@ function App() { const toggleLoopSelectedSegments = useCallback(() => togglePlay({ resetPlaybackRate: true, requestPlaybackMode: 'loop-selected-segments' }), [togglePlay]); const copySegmentsToClipboard = useCallback(async () => { - if (!isFileOpened) return; + if (!isFileOpened || selectedSegments.length === 0) return; electron.clipboard.writeText(await formatTsv(selectedSegments)); }, [isFileOpened, selectedSegments]); @@ -2255,7 +2255,7 @@ function App() { const onVideoClick = useCallback(() => togglePlay(), [togglePlay]); const tryExportEdlFile = useCallback(async (type: EdlExportType) => { - if (!checkFileOpened()) return; + if (!checkFileOpened() || selectedSegments.length === 0) return; await withErrorHandling(async () => { await exportEdlFile({ type, cutSegments: selectedSegments, customOutDir, filePath, getFrameCount }); }, i18n.t('Failed to export project')); diff --git a/src/renderer/src/SegmentList.tsx b/src/renderer/src/SegmentList.tsx index 1f69a483..c6e387a4 100644 --- a/src/renderer/src/SegmentList.tsx +++ b/src/renderer/src/SegmentList.tsx @@ -324,7 +324,6 @@ function SegmentList({ const segmentsTotal = useMemo(() => selectedSegments.reduce((acc, seg) => (seg.end == null ? 0 : seg.end - seg.start) + acc, 0), [selectedSegments]); - const segmentsOrInverse: (InverseCutSegment | StateSegment)[] = invertCutSegments ? inverseCutSegments : cutSegments; const sortableList = useMemo(() => segmentsOrInverse.map((seg) => ({ id: seg.segId, seg })), [segmentsOrInverse]); diff --git a/src/renderer/src/components/FileNameTemplateEditor.tsx b/src/renderer/src/components/FileNameTemplateEditor.tsx index 47597b26..afeb9c74 100644 --- a/src/renderer/src/components/FileNameTemplateEditor.tsx +++ b/src/renderer/src/components/FileNameTemplateEditor.tsx @@ -9,7 +9,7 @@ import { FaEdit } from 'react-icons/fa'; import { ReactSwal } from '../swal'; import HighlightedText from './HighlightedText'; -import { segNumVariable, segSuffixVariable, GenerateOutFileNames, extVariable, segTagsVariable, segNumIntVariable } from '../util/outputNameTemplate'; +import { segNumVariable, segSuffixVariable, GenerateOutFileNames, extVariable, segTagsVariable, segNumIntVariable, selectedSegNumVariable, selectedSegNumIntVariable } from '../util/outputNameTemplate'; import useUserSettings from '../hooks/useUserSettings'; import Switch from './Switch'; import Select from './Select'; @@ -48,7 +48,7 @@ function FileNameTemplateEditor(opts: { const { t } = useTranslation(); - const hasTextNumericPaddedValue = useMemo(() => [segNumVariable, segSuffixVariable].some((v) => debouncedText.includes(formatVariable(v))), [debouncedText]); + const hasTextNumericPaddedValue = useMemo(() => [segNumVariable, selectedSegNumVariable, segSuffixVariable].some((v) => debouncedText.includes(formatVariable(v))), [debouncedText]); useEffect(() => { if (debouncedText == null) { @@ -60,7 +60,7 @@ function FileNameTemplateEditor(opts: { (async () => { try { // console.time('generateFileNames') - const outSegs = await generateFileNames({ template: debouncedText }); + const outSegs = await generateFileNames(debouncedText); // console.timeEnd('generateOutSegFileNames') if (abortController.signal.aborted) return; setFileNames(outSegs.fileNames); @@ -78,7 +78,7 @@ function FileNameTemplateEditor(opts: { const availableVariables = useMemo(() => (mergeMode ? ['FILENAME', extVariable, 'EPOCH_MS', 'EXPORT_COUNT'] - : ['FILENAME', 'CUT_FROM', 'CUT_TO', segNumVariable, segNumIntVariable, 'SEG_LABEL', segSuffixVariable, extVariable, segTagsExample, 'EPOCH_MS', 'EXPORT_COUNT', 'FILE_EXPORT_COUNT'] + : ['FILENAME', 'CUT_FROM', 'CUT_TO', segNumVariable, segNumIntVariable, selectedSegNumVariable, selectedSegNumIntVariable, 'SEG_LABEL', segSuffixVariable, extVariable, segTagsExample, 'EPOCH_MS', 'EXPORT_COUNT', 'FILE_EXPORT_COUNT'] ), [mergeMode]); // eslint-disable-next-line no-template-curly-in-string diff --git a/src/renderer/src/hooks/useFfmpegOperations.ts b/src/renderer/src/hooks/useFfmpegOperations.ts index 3594e630..1b3ae60b 100644 --- a/src/renderer/src/hooks/useFfmpegOperations.ts +++ b/src/renderer/src/hooks/useFfmpegOperations.ts @@ -8,7 +8,7 @@ import { getSuffixedOutPath, transferTimestamps, getOutFileExtension, getOutDir, import { isCuttingStart, isCuttingEnd, runFfmpegWithProgress, getFfCommandLine, getDuration, createChaptersFromSegments, readFileMeta, getExperimentalArgs, getVideoTimescaleArgs, logStdoutStderr, runFfmpegConcat, RefuseOverwriteError, runFfmpeg } from '../ffmpeg'; import { getMapStreamsArgs, getStreamIdsToCopy } from '../util/streams'; import { getSmartCutParams } from '../smartcut'; -import { isDurationValid } from '../segments'; +import { getGuaranteedSegments, isDurationValid } from '../segments'; import { FFprobeStream } from '../../../../ffprobe'; import { AvoidNegativeTs, Html5ifyMode, PreserveMetadata } from '../../../../types'; import { AllFilesMeta, Chapter, CopyfileStreams, CustomTagsByFile, LiteFFprobeStream, ParamsByStreamId, SegmentToExport } from '../types'; @@ -503,7 +503,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea console.log('customTagsByFile', customTagsByFile); console.log('paramsByStreamId', paramsByStreamId); - const segments = segmentsIn.length > 0 ? segmentsIn : [{ start: 0, end: fileDuration ?? 0, name: '' }]; + const segments = getGuaranteedSegments(segmentsIn, fileDuration); const singleProgresses: Record = {}; function onSingleProgress(id: number, singleProgress: number) { diff --git a/src/renderer/src/hooks/useSegments.ts b/src/renderer/src/hooks/useSegments.ts index daf6fae8..e88dc398 100644 --- a/src/renderer/src/hooks/useSegments.ts +++ b/src/renderer/src/hooks/useSegments.ts @@ -173,7 +173,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea return { start, end }; }, [currentCutSeg, fileDurationNonZero]); - const selectedSegmentsRaw = useMemo(() => cutSegments.filter((segment) => segment.selected), [cutSegments]); + const selectedSegments = useMemo(() => cutSegments.flatMap((segment, i) => (segment.selected ? [{ ...segment, originalIndex: i }] : [])), [cutSegments]); const getFfmpegParameters = useCallback((key: FfmpegDialog) => { const parameters = ffmpegParameters[key]; @@ -272,9 +272,6 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea }); }, [cutSegments, fileDuration, haveInvalidSegs]); - // If user has selected none, default to all instead. - const selectedSegments = useMemo(() => (selectedSegmentsRaw.length > 0 ? selectedSegmentsRaw : cutSegments), [cutSegments, selectedSegmentsRaw]); - const invertAllSegments = useCallback(() => { // treat markers as 0 length const sortedSegments = sortSegments(selectedSegments.map(({ end, ...rest }) => ({ ...rest, end: end ?? rest.start }))); @@ -690,16 +687,16 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea }, [cutSegments, getScopeSegment, safeSetCutSegments]); const labelSelectedSegments = useCallback(async () => { - const firstSelectedSegment = selectedSegmentsRaw[0]; + const firstSelectedSegment = selectedSegments[0]; if (firstSelectedSegment == null) return; const { name } = firstSelectedSegment; const value = await labelSegmentDialog({ currentName: name, maxLength: maxLabelLength }); if (value == null) return; safeSetCutSegments((existingSegments) => existingSegments.map((existingSegment) => { - if (selectedSegmentsRaw.some((seg) => seg.segId === existingSegment.segId)) return { ...existingSegment, name: value }; + if (selectedSegments.some((seg) => seg.segId === existingSegment.segId)) return { ...existingSegment, name: value }; return existingSegment; })); - }, [maxLabelLength, selectedSegmentsRaw, safeSetCutSegments]); + }, [maxLabelLength, selectedSegments, safeSetCutSegments]); const maybeCreateFullLengthSegment = useCallback((newFileDuration: number) => { // don't use safeSetCutSegments because we want to set initial: true @@ -711,32 +708,37 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea }); }, [createIndexedSegment, setCutSegments]); - const segmentsOrInverse = useMemo<{ selected: DefiniteSegmentBase[], all: DefiniteSegmentBase[] }>(() => { + const segmentsOrInverse = useMemo<{ selected: SegmentToExport[], all: DefiniteSegmentBase[] }>(() => { // For invertCutSegments we do not support filtering (selecting) segments if (invertCutSegments) { return { - selected: inverseCutSegments, + selected: inverseCutSegments.map((seg, i) => ({ ...seg, originalIndex: i })), all: inverseCutSegments, }; } + // If user has selected no segments, default to all instead. + const selectedSegmentsWithFallback = selectedSegments.length > 0 ? selectedSegments : cutSegments.map((seg, i) => ({ ...seg, originalIndex: i })); + return { // exclude markers (segments without any end) // and exclude the initial segment, to prevent cutting when not really needed (if duration changes after the segment was created) - selected: filterNonMarkers(selectedSegments).filter((seg) => !seg.initial), + selected: filterNonMarkers(selectedSegmentsWithFallback).filter((seg) => !seg.initial), + // `all` includes also all non selected segments: all: filterNonMarkers(cutSegments).filter((seg) => !seg.initial), }; }, [cutSegments, inverseCutSegments, invertCutSegments, selectedSegments]); const segmentsToExport = useMemo(() => { - // segmentsToChaptersOnly is a special mode where all segments will be simply written out as chapters to one file: https://github.com/mifi/lossless-cut/issues/993#issuecomment-1037927595 - // Chapters export mode: Emulate a single segment with no cuts (full timeline) + // 'segmentsToChaptersOnly' is a special mode where all segments will be simply written out as chapters to one file: https://github.com/mifi/lossless-cut/issues/993#issuecomment-1037927595 + // Chapters export mode: Emulate no cuts (full timeline) if (segmentsToChaptersOnly) return []; + // in other modes, return all selected segments return segmentsOrInverse.selected; }, [segmentsOrInverse.selected, segmentsToChaptersOnly]); - const removeSelectedSegments = useCallback(() => removeSegments(selectedSegmentsRaw.map((seg) => seg.segId)), [removeSegments, selectedSegmentsRaw]); + const removeSelectedSegments = useCallback(() => removeSegments(selectedSegments.map((seg) => seg.segId)), [removeSegments, selectedSegments]); const selectOnlySegment = useCallback((seg: Pick) => setCutSegments((existing) => existing.map((segment) => ({ ...segment, selected: segment.segId === seg.segId, diff --git a/src/renderer/src/segments.ts b/src/renderer/src/segments.ts index 7e6e1487..2668c629 100644 --- a/src/renderer/src/segments.ts +++ b/src/renderer/src/segments.ts @@ -4,7 +4,7 @@ import minBy from 'lodash/minBy'; import maxBy from 'lodash/maxBy'; import invariant from 'tiny-invariant'; -import { PlaybackMode, SegmentBase, SegmentTags, StateSegment } from './types'; +import { PlaybackMode, SegmentBase, SegmentTags, SegmentToExport, StateSegment } from './types'; export const isDurationValid = (duration?: number): duration is number => duration != null && Number.isFinite(duration) && duration > 0; @@ -285,3 +285,5 @@ export function makeDurationSegments(segmentDuration: number, fileDuration: numb } export const isInitialSegment = (segments: StateSegment[]) => segments.length === 0 || (segments.length === 1 && segments[0]!.initial); + +export const getGuaranteedSegments = (segments: T[], fileDuration: number | undefined) => (segments.length > 0 ? segments : [{ start: 0, end: fileDuration ?? 0, name: '', originalIndex: 0 }]); diff --git a/src/renderer/src/types.ts b/src/renderer/src/types.ts index 9b0969ea..944d86f4 100644 --- a/src/renderer/src/types.ts +++ b/src/renderer/src/types.ts @@ -72,6 +72,7 @@ export interface StateSegment extends SegmentBase, SegmentColorIndex { } export interface SegmentToExport extends DefiniteSegmentBase { + originalIndex: number, name?: string | undefined; tags?: SegmentTags | undefined; } diff --git a/src/renderer/src/util/outputNameTemplate.ts b/src/renderer/src/util/outputNameTemplate.ts index 1bc67030..a90bff14 100644 --- a/src/renderer/src/util/outputNameTemplate.ts +++ b/src/renderer/src/util/outputNameTemplate.ts @@ -1,16 +1,19 @@ import i18n from 'i18next'; import { PlatformPath } from 'node:path'; import pMap from 'p-map'; +import max from 'lodash/max'; import { isMac, isWindows, hasDuplicates, filenamify, getOutFileExtension } from '../util'; import isDev from '../isDev'; -import { getSegmentTags, formatSegNum } from '../segments'; +import { getSegmentTags, formatSegNum, getGuaranteedSegments } from '../segments'; import { FormatTimecode, SegmentToExport } from '../types'; import safeishEval from '../worker/eval'; export const segNumVariable = 'SEG_NUM'; export const segNumIntVariable = 'SEG_NUM_INT'; +export const selectedSegNumVariable = 'SELECTED_SEG_NUM'; +export const selectedSegNumIntVariable = 'SELECTED_SEG_NUM_INT'; export const segSuffixVariable = 'SEG_SUFFIX'; export const extVariable = 'EXT'; export const segTagsVariable = 'SEG_TAGS'; @@ -111,7 +114,7 @@ export const defaultCutMergedFileTemplate = '${FILENAME}-cut-merged-${EPOCH_MS}$ // eslint-disable-next-line no-template-curly-in-string export const defaultMergedFileTemplate = '${FILENAME}-merged-${EPOCH_MS}${EXT}'; -async function interpolateOutFileName(template: string, { epochMs, inputFileNameWithoutExt, ext, segSuffix, segNum, segNumPadded, segLabel, cutFrom, cutTo, tags, exportCount, currentFileExportCount }: { +async function interpolateOutFileName(template: string, { epochMs, inputFileNameWithoutExt, ext, segSuffix, segNum, segNumPadded, selectedSegNum, selectedSegNumPadded, segLabel, cutFrom, cutTo, tags, exportCount, currentFileExportCount }: { epochMs: number, inputFileNameWithoutExt: string, ext: string, @@ -121,6 +124,8 @@ async function interpolateOutFileName(template: string, { epochMs, inputFileName segSuffix: string, segNum: number, segNumPadded: string, + selectedSegNum: number, + selectedSegNumPadded: string, segLabel: string, cutFrom: string, cutTo: string, @@ -131,7 +136,9 @@ async function interpolateOutFileName(template: string, { epochMs, inputFileName [segSuffixVariable]: segSuffix, [extVariable]: ext, [segNumIntVariable]: segNum, - [segNumVariable]: segNumPadded, // todo rename this (breaking change) + [segNumVariable]: segNumPadded, + [selectedSegNumIntVariable]: selectedSegNum, + [selectedSegNumVariable]: selectedSegNumPadded, SEG_LABEL: segLabel, EPOCH_MS: epochMs, CUT_FROM: cutFrom, @@ -164,9 +171,9 @@ function maybeTruncatePath(fileName: string, truncate: boolean) { ].join(pathSep); } -export async function generateOutSegFileNames({ fileDuration, segments: segmentsIn, template: desiredTemplate, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding, exportCount, currentFileExportCount }: { +export async function generateOutSegFileNames({ fileDuration, segmentsToExport: segmentsToExportIn, template: desiredTemplate, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding, exportCount, currentFileExportCount }: { fileDuration: number | undefined, - segments: SegmentToExport[], + segmentsToExport: SegmentToExport[], template: string, formatTimecode: FormatTimecode, isCustomFormatSelected: boolean, @@ -179,15 +186,19 @@ export async function generateOutSegFileNames({ fileDuration, segments: segments exportCount: number, currentFileExportCount: number, }) { + const segmentsToExport = getGuaranteedSegments(segmentsToExportIn, fileDuration); + async function generate({ template, forceSafeOutputFileName }: { template: string, forceSafeOutputFileName: boolean }) { const epochMs = Date.now(); - const segments = segmentsIn.length > 0 ? segmentsIn : [{ start: 0, end: fileDuration ?? 0, name: '' }]; + const maxOriginalIndex = max(segmentsToExport.map((s) => s.originalIndex)) ?? 0; - return pMap(segments, async (segment, i) => { + return pMap(segmentsToExport, async (segment, i) => { const { start, end, name = '' } = segment; - const segNum = i + 1; - const segNumPadded = formatSegNum(i, segments.length, outputFileNameMinZeroPadding); + const selectedSegNum = i + 1; + const selectedSegNumPadded = formatSegNum(i, segmentsToExport.length, outputFileNameMinZeroPadding); + const segNum = segment.originalIndex + 1; + const segNumPadded = formatSegNum(segment.originalIndex, maxOriginalIndex + 1, outputFileNameMinZeroPadding); // Fields that did not come from the source file's name must be sanitized, because they may contain characters that are not supported by the target operating/file system // however we disable this when the user has chosen to (safeOutputFileName === false) @@ -196,7 +207,7 @@ export async function generateOutSegFileNames({ fileDuration, segments: segments function getSegSuffix() { if (name) return `-${filenamifyOrNot(name)}`; // https://github.com/mifi/lossless-cut/issues/583 - if (segments.length > 1) return `-seg${segNumPadded}`; + if (segmentsToExport.length > 1) return `-seg${segNumPadded}`; return ''; } @@ -206,8 +217,10 @@ export async function generateOutSegFileNames({ fileDuration, segments: segments epochMs, segNum, segNumPadded, - inputFileNameWithoutExt, + selectedSegNum, + selectedSegNumPadded, segSuffix: getSegSuffix(), + inputFileNameWithoutExt, ext: getOutFileExtension({ isCustomFormatSelected, outFormat: fileFormat, filePath }), segLabel: filenamifyOrNot(name), cutFrom: formatTimecode({ seconds: start, fileNameFriendly: true }), @@ -231,7 +244,7 @@ export async function generateOutSegFileNames({ fileDuration, segments: segments return { fileNames, problems }; } -export type GenerateOutFileNames = (a: { template: string }) => Promise<{ +export type GenerateOutFileNames = (template: string) => Promise<{ fileNames: string[], problems: { error: string | undefined;