don't delete existing segments when not overwriting

closes #2436
pull/2465/head
Mikael Finstad 2025-05-05 22:21:38 +02:00
rodzic 250505e9cd
commit 2505053f1d
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 25AB36E3E81CBC26
2 zmienionych plików z 47 dodań i 43 usunięć

Wyświetl plik

@ -646,7 +646,7 @@ function App() {
const needSmartCut = !!(areWeCutting && enableSmartCut);
const {
concatFiles, html5ifyDummy, cutMultiple, autoConcatCutSegments, html5ify, fixInvalidDuration, extractStreams,
concatFiles, html5ifyDummy, cutMultiple, concatCutSegments, html5ify, fixInvalidDuration, extractStreams, tryDeleteFiles,
} = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, smartCutCustomBitrate: smartCutBitrate, appendFfmpegCommandLog });
const { captureFrameFromTag, captureFrameFromFfmpeg, captureFramesRange } = useFrameCapture({ appendFfmpegCommandLog, formatTimecode, treatOutputFileModifiedTimeAsStart });
@ -1133,19 +1133,22 @@ function App() {
invariant(fileName != null);
mergedOutFilePath = getOutPath({ customOutDir, filePath, fileName });
await autoConcatCutSegments({
await concatCutSegments({
customOutDir,
outFormat: fileFormat,
segmentPaths: outFiles,
segmentPaths: outFiles.map((f) => f.path),
ffmpegExperimental,
preserveMovData,
movFastStart,
onProgress: setProgress,
chapterNames,
autoDeleteMergedSegments,
preserveMetadataOnMerge,
mergedOutFilePath,
});
// don't delete existing files that were not created by losslesscut now (due to overwrite disabled) https://github.com/mifi/lossless-cut/issues/2436
const createdOutFiles = outFiles.flatMap((f) => (f.created ? [f.path] : []));
if (autoDeleteMergedSegments) await tryDeleteFiles(createdOutFiles);
}
if (!enableOverwriteOutput) warnings.push(i18n.t('Overwrite output setting is disabled and some files might have been skipped.'));
@ -1173,8 +1176,7 @@ function App() {
if (areWeCutting) notices.push(i18n.t('Cutpoints may be inaccurate.'));
const revealPath = willMerge && mergedOutFilePath != null ? mergedOutFilePath : outFiles[0];
invariant(revealPath != null);
const revealPath = willMerge && mergedOutFilePath != null ? mergedOutFilePath : outFiles[0]!.path;
if (!hideAllNotifications) {
showOsNotification(i18n.t('Export finished'));
openExportFinishedToast({ filePath: revealPath, warnings, notices });
@ -1213,7 +1215,7 @@ function App() {
setWorking(undefined);
setProgress(undefined);
}
}, [filePath, numStreamsToCopy, segmentsToExport, haveInvalidSegs, workingRef, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, cutMultiple, outputDir, customOutDir, fileFormat, fileDuration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMetadataOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, detectedFps, willMerge, enableOverwriteOutput, exportConfirmEnabled, mainFileFormatData, mainStreams, exportExtraStreams, areWeCutting, hideAllNotifications, cleanupChoices.cleanupAfterExport, cleanupFilesWithDialog, segmentsOrInverse, t, mergedFileTemplateOrDefault, segmentsToChapters, invertCutSegments, generateMergedFileNames, autoConcatCutSegments, autoDeleteMergedSegments, nonCopiedExtraStreams, extractStreams, showOsNotification, handleExportFailed]);
}, [filePath, numStreamsToCopy, haveInvalidSegs, workingRef, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, cutMultiple, outputDir, customOutDir, fileFormat, fileDuration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, segmentsToExport, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMetadataOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, detectedFps, willMerge, enableOverwriteOutput, exportConfirmEnabled, mainFileFormatData, mainStreams, exportExtraStreams, areWeCutting, hideAllNotifications, cleanupChoices.cleanupAfterExport, cleanupFilesWithDialog, segmentsOrInverse.selected, t, mergedFileTemplateOrDefault, segmentsToChapters, invertCutSegments, generateMergedFileNames, concatCutSegments, autoDeleteMergedSegments, tryDeleteFiles, nonCopiedExtraStreams, extractStreams, showOsNotification, handleExportFailed]);
const onExportPress = useCallback(async () => {
if (!filePath) return;

Wyświetl plik

@ -258,8 +258,6 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
videoTimebase?: number | undefined,
detectedFps?: number,
}) => {
if (await shouldSkipExistingFile(outPath)) return;
const frameDuration = getFrameDuration(detectedFps);
const cuttingStart = isCuttingStart(cutFrom);
@ -417,11 +415,21 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
logStdoutStderr(result);
await transferTimestamps({ inPath: filePath, outPath, cutFrom, cutTo, treatInputFileModifiedTimeAsStart, duration: isDurationValid(fileDuration) ? fileDuration : undefined, treatOutputFileModifiedTimeAsStart });
}, [appendFfmpegCommandLog, cutFromAdjustmentFrames, cutToAdjustmentFrames, filePath, getOutputPlaybackRateArgs, shouldSkipExistingFile, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);
}, [appendFfmpegCommandLog, cutFromAdjustmentFrames, cutToAdjustmentFrames, filePath, getOutputPlaybackRateArgs, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);
// inspired by https://gist.github.com/fernandoherreradelasheras/5eca67f4200f1a7cc8281747da08496e
const cutEncodeSmartPart = useCallback(async ({ cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate, videoTimebase, allFilesMeta, copyFileStreams, videoStreamIndex, ffmpegExperimental }: {
cutFrom: number, cutTo: number, outPath: string, outFormat: string, videoCodec: string, videoBitrate: number, videoTimebase: number, allFilesMeta: AllFilesMeta, copyFileStreams: CopyfileStreams, videoStreamIndex: number, ffmpegExperimental: boolean,
cutFrom: number,
cutTo: number,
outPath: string,
outFormat: string,
videoCodec: string,
videoBitrate: number,
videoTimebase: number,
allFilesMeta: AllFilesMeta,
copyFileStreams: CopyfileStreams,
videoStreamIndex: number,
ffmpegExperimental: boolean,
}) => {
invariant(filePath != null);
@ -517,22 +525,22 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// or if enabled, will first cut&encode the part before the next keyframe, trying to match the input file's codec params
// then it will cut the part *from* the keyframe to "end", and concat them together and return the concated file
// so that for the calling code it looks as if it's just a normal segment
async function maybeSmartCutSegment({ start: desiredCutFrom, end: cutTo }: { start: number, end: number }, i: number) {
async function makeSegmentOutPath() {
const outPath = join(outputDir, outSegFileNames[i]!);
// because outSegFileNames might contain slashes https://github.com/mifi/lossless-cut/issues/1532
const actualOutputDir = dirname(outPath);
if (actualOutputDir !== outputDir) await mkdir(actualOutputDir, { recursive: true });
return outPath;
}
async function cutSegment({ start: desiredCutFrom, end: cutTo }: { start: number, end: number }, i: number) {
const finalOutPath = join(outputDir, outSegFileNames[i]!);
if (await shouldSkipExistingFile(finalOutPath)) return { path: finalOutPath, created: false };
// outSegFileNames might contain slashes and therefore might have a subdir(tree) that we need to mkdir
// https://github.com/mifi/lossless-cut/issues/1532
const actualOutputDir = dirname(finalOutPath);
if (actualOutputDir !== outputDir) await mkdir(actualOutputDir, { recursive: true });
if (!needSmartCut) {
const outPath = await makeSegmentOutPath();
invariant(outFormat != null);
await losslessCutSingle({
cutFrom: desiredCutFrom, cutTo, chaptersPath, outPath, copyFileStreams, keyframeCut, avoidNegativeTs, fileDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, customTagsByFile, paramsByStreamId, onProgress: (progress) => onSingleProgress(i, progress),
cutFrom: desiredCutFrom, cutTo, chaptersPath, outPath: finalOutPath, copyFileStreams, keyframeCut, avoidNegativeTs, fileDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, customTagsByFile, paramsByStreamId, onProgress: (progress) => onSingleProgress(i, progress),
});
return outPath;
return { path: finalOutPath, created: true };
}
invariant(filePath != null);
@ -572,31 +580,32 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// If we are cutting within two keyframes, just encode the whole part and return that
// See https://github.com/mifi/lossless-cut/pull/1267#issuecomment-1236381740
if (segmentNeedsSmartCut && losslessCutFrom > cutTo) {
const outPath = await makeSegmentOutPath();
console.log('Segment is between two keyframes, cutting/encoding the whole segment', { desiredCutFrom, losslessCutFrom, cutTo });
await cutEncodeSmartPartWrapper({ cutFrom: desiredCutFrom, cutTo, outPath });
return outPath;
await cutEncodeSmartPartWrapper({ cutFrom: desiredCutFrom, cutTo, outPath: finalOutPath });
return { path: finalOutPath, created: true };
}
invariant(outFormat != null);
const ext = getOutFileExtension({ isCustomFormatSelected: true, outFormat, filePath });
const losslessPartOutPath = segmentNeedsSmartCut
? getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `smartcut-segment-copy-${i}${ext}` })
: await makeSegmentOutPath();
if (segmentNeedsSmartCut) {
console.log('Cutting/encoding lossless part', { from: losslessCutFrom, to: cutTo });
}
const losslessPartOutPath = segmentNeedsSmartCut
? getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `smartcut-segment-copy-${i}${ext}` })
: finalOutPath;
// for smart cut we need to use keyframe cut here, and no avoid_negative_ts
await losslessCutSingle({
cutFrom: losslessCutFrom, cutTo, chaptersPath, outPath: losslessPartOutPath, copyFileStreams: copyFileStreamsFiltered, keyframeCut: true, avoidNegativeTs: undefined, fileDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, customTagsByFile, paramsByStreamId, videoTimebase, onProgress,
});
// OK, just return the single cut file (we may need smart cut in other segments though)
if (!segmentNeedsSmartCut) return losslessPartOutPath;
// We don't need to concat, just return the single cut file (we may need smart cut in other segments though)
if (!segmentNeedsSmartCut) return { path: finalOutPath, created: true };
// We need to concat
const smartCutEncodedPartOutPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `smartcut-segment-encode-${i}${ext}` });
const smartCutSegmentsToConcat = [smartCutEncodedPartOutPath, losslessPartOutPath];
@ -607,31 +616,26 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const encodeCutToSafe = Math.max(desiredCutFrom + frameDuration, losslessCutFrom - frameDuration);
console.log('Cutting/encoding smart part', { from: desiredCutFrom, to: encodeCutToSafe });
await cutEncodeSmartPartWrapper({ cutFrom: desiredCutFrom, cutTo: encodeCutToSafe, outPath: smartCutEncodedPartOutPath });
// need to re-read streams because indexes may have changed. Using main file as source of streams and metadata
const { streams: streamsAfterCut } = await readFileMeta(losslessPartOutPath);
const outPath = await makeSegmentOutPath();
await concatFiles({ paths: smartCutSegmentsToConcat, outDir: outputDir, outPath, metadataFromPath: losslessPartOutPath, outFormat, includeAllStreams: true, streams: streamsAfterCut, ffmpegExperimental, preserveMovData, movFastStart, chapters, preserveMetadataOnMerge, videoTimebase, onProgress: onConcatProgress });
return outPath;
await concatFiles({ paths: smartCutSegmentsToConcat, outDir: outputDir, outPath: finalOutPath, metadataFromPath: losslessPartOutPath, outFormat, includeAllStreams: true, streams: streamsAfterCut, ffmpegExperimental, preserveMovData, movFastStart, chapters, preserveMetadataOnMerge, videoTimebase, onProgress: onConcatProgress });
return { path: finalOutPath, created: true };
} finally {
await tryDeleteFiles(smartCutSegmentsToConcat);
}
}
try {
const outFiles = await pMap(segments, maybeSmartCutSegment, { concurrency: 1 });
return outFiles;
return await pMap(segments, cutSegment, { concurrency: 1 });
} finally {
if (chaptersPath) await tryDeleteFiles([chaptersPath]);
}
}, [needSmartCut, filePath, losslessCutSingle, shouldSkipExistingFile, cutEncodeSmartPart, smartCutCustomBitrate, concatFiles]);
const autoConcatCutSegments = useCallback(async ({ customOutDir, outFormat, segmentPaths, ffmpegExperimental, onProgress, preserveMovData, movFastStart, autoDeleteMergedSegments, chapterNames, preserveMetadataOnMerge, mergedOutFilePath }: {
const concatCutSegments = useCallback(async ({ customOutDir, outFormat, segmentPaths, ffmpegExperimental, onProgress, preserveMovData, movFastStart, chapterNames, preserveMetadataOnMerge, mergedOutFilePath }: {
customOutDir: string | undefined,
outFormat: string | undefined,
segmentPaths: string[],
@ -639,7 +643,6 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
onProgress: (p: number) => void,
preserveMovData: boolean,
movFastStart: boolean,
autoDeleteMergedSegments: boolean,
chapterNames: (string | undefined)[] | undefined,
preserveMetadataOnMerge: boolean,
mergedOutFilePath: string,
@ -655,7 +658,6 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// need to re-read streams because may have changed
const { streams } = await readFileMeta(metadataFromPath);
await concatFiles({ paths: segmentPaths, outDir, outPath: mergedOutFilePath, metadataFromPath, outFormat, includeAllStreams: true, streams, ffmpegExperimental, onProgress, preserveMovData, movFastStart, chapters, preserveMetadataOnMerge });
if (autoDeleteMergedSegments) await tryDeleteFiles(segmentPaths);
}, [concatFiles, filePath, shouldSkipExistingFile]);
const html5ify = useCallback(async ({ customOutDir, filePath: filePathArg, speed, hasAudio, hasVideo, onProgress }: {
@ -978,7 +980,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
}, [extractAttachmentStreams, extractNonAttachmentStreams, filePath]);
return {
cutMultiple, concatFiles, html5ify, html5ifyDummy, fixInvalidDuration, autoConcatCutSegments, extractStreams,
cutMultiple, concatFiles, html5ify, html5ifyDummy, fixInvalidDuration, concatCutSegments, extractStreams, tryDeleteFiles,
};
}