diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 3f262dd0..a3e959bd 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -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; diff --git a/src/renderer/src/hooks/useFfmpegOperations.ts b/src/renderer/src/hooks/useFfmpegOperations.ts index 206d1716..4fdf2353 100644 --- a/src/renderer/src/hooks/useFfmpegOperations.ts +++ b/src/renderer/src/hooks/useFfmpegOperations.ts @@ -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, }; }