From 9161278e54ac32aa7205249bcadd44b1206e3374 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Fri, 12 Aug 2022 20:54:33 +0200 Subject: [PATCH] add a setting for overwriting output file fixes #916 --- public/configStore.js | 1 + src/App.jsx | 36 +++++++++++++++++++--------- src/Settings.jsx | 13 +++++++++- src/dialogs.jsx | 7 ++++++ src/ffmpeg.js | 41 ++++++++++++++++++++++---------- src/hooks/useFfmpegOperations.js | 11 ++++++++- src/hooks/useUserSettingsRoot.js | 4 ++++ 7 files changed, 87 insertions(+), 26 deletions(-) diff --git a/public/configStore.js b/public/configStore.js index 24fbf81..2949a4c 100644 --- a/public/configStore.js +++ b/public/configStore.js @@ -110,6 +110,7 @@ const defaults = { keyBindings: defaultKeyBindings, customFfPath: undefined, storeProjectInWorkingDir: true, + enableOverwriteOutput: true, }; // For portable app: https://github.com/mifi/lossless-cut/issues/645 diff --git a/src/App.jsx b/src/App.jsx index 0172dc3..be65190 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -56,7 +56,7 @@ import { extractStreams, runStartupCheck, setCustomFfPath as ffmpegSetCustomFfPath, isIphoneHevc, tryMapChaptersToEdl, blackDetect, getDuration, getTimecodeFromStreams, createChaptersFromSegments, extractSubtitleTrack, - getFfmpegPath, + getFfmpegPath, RefuseOverwriteError, } from './ffmpeg'; import { shouldCopyStreamByDefault, getAudioStreams, getRealVideoStreams, defaultProcessedCodecTypes, isAudioDefinitelyNotSupported, doesPlayerSupportFile } from './util/streams'; import { exportEdlFile, readEdlFile, saveLlcProject, loadLlcProject, askForEdlImport } from './edlStore'; @@ -70,7 +70,7 @@ import { } from './util'; import { formatDuration } from './util/duration'; import { adjustRate } from './util/rate-calculator'; -import { askForOutDir, askForInputDir, askForImportChapters, createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, openAbout, showEditableJsonDialog, askForShiftSegments, selectSegmentsByLabelDialog, confirmExtractFramesAsImages } from './dialogs'; +import { askForOutDir, askForInputDir, askForImportChapters, createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, openAbout, showEditableJsonDialog, askForShiftSegments, selectSegmentsByLabelDialog, confirmExtractFramesAsImages, showRefuseToOverwrite } from './dialogs'; import { openSendReportDialog } from './reporting'; import { fallbackLng } from './i18n'; import { createSegment, getCleanCutSegments, getSegApparentStart, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, convertSegmentsToChapters, hasAnySegmentOverlap } from './segments'; @@ -200,7 +200,7 @@ const App = memo(() => { const zoomedDuration = isDurationValid(duration) ? duration / zoom : undefined; const { - captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, setAvoidNegativeTs, autoMerge, setAutoMerge, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, setAutoExportExtraStreams, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, setAutoSaveProjectFile, wheelSensitivity, setWheelSensitivity, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, ffmpegExperimental, setFfmpegExperimental, hideNotifications, setHideNotifications, autoLoadTimecode, setAutoLoadTimecode, autoDeleteMergedSegments, setAutoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, simpleMode, setSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, setKeyboardSeekAccFactor, keyboardNormalSeekSpeed, setKeyboardNormalSeekSpeed, enableTransferTimestamps, setEnableTransferTimestamps, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, setEnableAutoHtml5ify, segmentsToChaptersOnly, setSegmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, setEnableSmartCut, customFfPath, setCustomFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, + captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, setAvoidNegativeTs, autoMerge, setAutoMerge, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, setAutoExportExtraStreams, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, setAutoSaveProjectFile, wheelSensitivity, setWheelSensitivity, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, ffmpegExperimental, setFfmpegExperimental, hideNotifications, setHideNotifications, autoLoadTimecode, setAutoLoadTimecode, autoDeleteMergedSegments, setAutoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, simpleMode, setSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, setKeyboardSeekAccFactor, keyboardNormalSeekSpeed, setKeyboardNormalSeekSpeed, enableTransferTimestamps, setEnableTransferTimestamps, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, setEnableAutoHtml5ify, segmentsToChaptersOnly, setSegmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, setEnableSmartCut, customFfPath, setCustomFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, setEnableOverwriteOutput, } = useUserSettingsRoot(); useEffect(() => { @@ -754,8 +754,8 @@ const App = memo(() => { }, [autoDeleteMergedSegments, autoMerge, segmentsToChaptersOnly]); const userSettingsContext = useMemo(() => ({ - captureFormat, setCaptureFormat, toggleCaptureFormat, customOutDir, setCustomOutDir, changeOutDir, keyframeCut, setKeyframeCut, toggleKeyframeCut, preserveMovData, setPreserveMovData, togglePreserveMovData, movFastStart, setMovFastStart, toggleMovFastStart, avoidNegativeTs, setAvoidNegativeTs, autoMerge, setAutoMerge, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, setAutoExportExtraStreams, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, setAutoSaveProjectFile, wheelSensitivity, setWheelSensitivity, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, ffmpegExperimental, setFfmpegExperimental, hideNotifications, setHideNotifications, autoLoadTimecode, setAutoLoadTimecode, autoDeleteMergedSegments, setAutoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, toggleSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, togglePreserveMetadataOnMerge, simpleMode, setSimpleMode, toggleSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, setKeyboardSeekAccFactor, keyboardNormalSeekSpeed, setKeyboardNormalSeekSpeed, enableTransferTimestamps, setEnableTransferTimestamps, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, toggleSafeOutputFileName, enableAutoHtml5ify, setEnableAutoHtml5ify, segmentsToChaptersOnly, setSegmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, setEnableSmartCut, customFfPath, setCustomFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, effectiveExportMode, - }), [askBeforeClose, autoDeleteMergedSegments, autoExportExtraStreams, autoLoadTimecode, autoMerge, autoSaveProjectFile, avoidNegativeTs, captureFormat, changeOutDir, customFfPath, customOutDir, effectiveExportMode, enableAskForFileOpenAction, enableAskForImportChapters, enableAutoHtml5ify, enableSmartCut, enableTransferTimestamps, exportConfirmEnabled, ffmpegExperimental, hideNotifications, invertCutSegments, invertTimelineScroll, keyBindings, keyboardNormalSeekSpeed, keyboardSeekAccFactor, keyframeCut, language, movFastStart, outFormatLocked, outSegTemplate, playbackVolume, preserveMetadataOnMerge, preserveMovData, resetKeyBindings, safeOutputFileName, segmentsToChapters, segmentsToChaptersOnly, setAskBeforeClose, setAutoDeleteMergedSegments, setAutoExportExtraStreams, setAutoLoadTimecode, setAutoMerge, setAutoSaveProjectFile, setAvoidNegativeTs, setCaptureFormat, setCustomFfPath, setCustomOutDir, setEnableAskForFileOpenAction, setEnableAskForImportChapters, setEnableAutoHtml5ify, setEnableSmartCut, setEnableTransferTimestamps, setExportConfirmEnabled, setFfmpegExperimental, setHideNotifications, setInvertCutSegments, setInvertTimelineScroll, setKeyBindings, setKeyboardNormalSeekSpeed, setKeyboardSeekAccFactor, setKeyframeCut, setLanguage, setMovFastStart, setOutFormatLocked, setOutSegTemplate, setPlaybackVolume, setPreserveMetadataOnMerge, setPreserveMovData, setSafeOutputFileName, setSegmentsToChapters, setSegmentsToChaptersOnly, setSimpleMode, setStoreProjectInWorkingDir, setTimecodeFormat, setWheelSensitivity, simpleMode, storeProjectInWorkingDir, timecodeFormat, toggleCaptureFormat, toggleExportConfirmEnabled, toggleKeyframeCut, toggleMovFastStart, togglePreserveMetadataOnMerge, togglePreserveMovData, toggleSafeOutputFileName, toggleSegmentsToChapters, toggleSimpleMode, wheelSensitivity]); + captureFormat, setCaptureFormat, toggleCaptureFormat, customOutDir, setCustomOutDir, changeOutDir, keyframeCut, setKeyframeCut, toggleKeyframeCut, preserveMovData, setPreserveMovData, togglePreserveMovData, movFastStart, setMovFastStart, toggleMovFastStart, avoidNegativeTs, setAvoidNegativeTs, autoMerge, setAutoMerge, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, setAutoExportExtraStreams, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, setAutoSaveProjectFile, wheelSensitivity, setWheelSensitivity, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, ffmpegExperimental, setFfmpegExperimental, hideNotifications, setHideNotifications, autoLoadTimecode, setAutoLoadTimecode, autoDeleteMergedSegments, setAutoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, toggleSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, togglePreserveMetadataOnMerge, simpleMode, setSimpleMode, toggleSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, setKeyboardSeekAccFactor, keyboardNormalSeekSpeed, setKeyboardNormalSeekSpeed, enableTransferTimestamps, setEnableTransferTimestamps, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, toggleSafeOutputFileName, enableAutoHtml5ify, setEnableAutoHtml5ify, segmentsToChaptersOnly, setSegmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, setEnableSmartCut, customFfPath, setCustomFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, effectiveExportMode, enableOverwriteOutput, setEnableOverwriteOutput, + }), [askBeforeClose, autoDeleteMergedSegments, autoExportExtraStreams, autoLoadTimecode, autoMerge, autoSaveProjectFile, avoidNegativeTs, captureFormat, changeOutDir, customFfPath, customOutDir, effectiveExportMode, enableAskForFileOpenAction, enableAskForImportChapters, enableAutoHtml5ify, enableOverwriteOutput, enableSmartCut, enableTransferTimestamps, exportConfirmEnabled, ffmpegExperimental, hideNotifications, invertCutSegments, invertTimelineScroll, keyBindings, keyboardNormalSeekSpeed, keyboardSeekAccFactor, keyframeCut, language, movFastStart, outFormatLocked, outSegTemplate, playbackVolume, preserveMetadataOnMerge, preserveMovData, resetKeyBindings, safeOutputFileName, segmentsToChapters, segmentsToChaptersOnly, setAskBeforeClose, setAutoDeleteMergedSegments, setAutoExportExtraStreams, setAutoLoadTimecode, setAutoMerge, setAutoSaveProjectFile, setAvoidNegativeTs, setCaptureFormat, setCustomFfPath, setCustomOutDir, setEnableAskForFileOpenAction, setEnableAskForImportChapters, setEnableAutoHtml5ify, setEnableOverwriteOutput, setEnableSmartCut, setEnableTransferTimestamps, setExportConfirmEnabled, setFfmpegExperimental, setHideNotifications, setInvertCutSegments, setInvertTimelineScroll, setKeyBindings, setKeyboardNormalSeekSpeed, setKeyboardSeekAccFactor, setKeyframeCut, setLanguage, setMovFastStart, setOutFormatLocked, setOutSegTemplate, setPlaybackVolume, setPreserveMetadataOnMerge, setPreserveMovData, setSafeOutputFileName, setSegmentsToChapters, setSegmentsToChaptersOnly, setSimpleMode, setStoreProjectInWorkingDir, setTimecodeFormat, setWheelSensitivity, simpleMode, storeProjectInWorkingDir, timecodeFormat, toggleCaptureFormat, toggleExportConfirmEnabled, toggleKeyframeCut, toggleMovFastStart, togglePreserveMetadataOnMerge, togglePreserveMovData, toggleSafeOutputFileName, toggleSegmentsToChapters, toggleSimpleMode, wheelSensitivity]); const isCopyingStreamId = useCallback((path, streamId) => ( !!(copyStreamIdsByFile[path] || {})[streamId] @@ -1323,6 +1323,7 @@ const App = memo(() => { chapters: chaptersToAdd, detectedFps, enableSmartCut, + enableOverwriteOutput, }); if (willMerge) { @@ -1354,7 +1355,7 @@ const App = memo(() => { if (exportExtraStreams) { try { - await extractStreams({ filePath, customOutDir, streams: nonCopiedExtraStreams }); + await extractStreams({ filePath, customOutDir, streams: nonCopiedExtraStreams, enableOverwriteOutput }); msgs.push(i18n.t('Unprocessable streams were exported as separate files.')); } catch (err) { console.error('Extra stream export failed', err); @@ -1363,6 +1364,11 @@ const App = memo(() => { if (!hideAllNotifications) openDirToast({ dirPath: outputDir, text: msgs.join(' '), timer: 15000 }); } catch (err) { + if (err instanceof RefuseOverwriteError) { + showRefuseToOverwrite(); + return; + } + console.error('stdout:', err.stdout); console.error('stderr:', err.stderr); @@ -1380,7 +1386,7 @@ const App = memo(() => { setWorking(); setCutProgress(); } - }, [numStreamsToCopy, setWorking, segmentsToChaptersOnly, selectedSegmentsOrInverse, outSegTemplateOrDefault, generateOutSegFileNames, segmentsToExport, getOutSegError, cutMultiple, outputDir, customOutDir, fileFormat, duration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, shortestFlag, ffmpegExperimental, preserveMovData, preserveMetadataOnMerge, movFastStart, avoidNegativeTs, customTagsByFile, customTagsByStreamId, dispositionByStreamId, detectedFps, enableSmartCut, willMerge, mainFileFormatData, mainStreams, exportExtraStreams, hideAllNotifications, segmentsToChapters, invertCutSegments, autoConcatCutSegments, isCustomFormatSelected, autoDeleteMergedSegments, filePath, nonCopiedExtraStreams, handleCutFailed]); + }, [numStreamsToCopy, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, segmentsToExport, getOutSegError, cutMultiple, outputDir, customOutDir, fileFormat, duration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, shortestFlag, ffmpegExperimental, preserveMovData, preserveMetadataOnMerge, movFastStart, avoidNegativeTs, customTagsByFile, customTagsByStreamId, dispositionByStreamId, detectedFps, enableSmartCut, enableOverwriteOutput, willMerge, mainFileFormatData, mainStreams, exportExtraStreams, hideAllNotifications, selectedSegmentsOrInverse, segmentsToChapters, invertCutSegments, autoConcatCutSegments, isCustomFormatSelected, autoDeleteMergedSegments, filePath, nonCopiedExtraStreams, handleCutFailed]); const onExportPress = useCallback(async () => { if (!filePath || workingRef.current || segmentsToExport.length < 1) return; @@ -1736,15 +1742,19 @@ const App = memo(() => { try { setWorking(i18n.t('Extracting all streams')); setStreamsSelectorShown(false); - await extractStreams({ customOutDir, filePath, streams: mainStreams }); + await extractStreams({ customOutDir, filePath, streams: mainStreams, enableOverwriteOutput }); openDirToast({ dirPath: outputDir, text: i18n.t('All streams have been extracted as separate files') }); } catch (err) { + if (err instanceof RefuseOverwriteError) { + showRefuseToOverwrite(); + return; + } errorToast(i18n.t('Failed to extract all streams')); console.error('Failed to extract all streams', err); } finally { setWorking(); } - }, [customOutDir, filePath, mainStreams, outputDir, setWorking]); + }, [customOutDir, enableOverwriteOutput, filePath, mainStreams, outputDir, setWorking]); const detectBlackScenes = useCallback(async () => { if (!filePath) return; @@ -2007,15 +2017,19 @@ const App = memo(() => { try { setWorking(i18n.t('Extracting track')); // setStreamsSelectorShown(false); - await extractStreams({ customOutDir, filePath, streams: mainStreams.filter((s) => s.index === index) }); + await extractStreams({ customOutDir, filePath, streams: mainStreams.filter((s) => s.index === index), enableOverwriteOutput }); openDirToast({ dirPath: outputDir, text: i18n.t('Track has been extracted') }); } catch (err) { + if (err instanceof RefuseOverwriteError) { + showRefuseToOverwrite(); + return; + } errorToast(i18n.t('Failed to extract track')); console.error('Failed to extract track', err); } finally { setWorking(); } - }, [customOutDir, filePath, mainStreams, outputDir, setWorking]); + }, [customOutDir, enableOverwriteOutput, filePath, mainStreams, outputDir, setWorking]); const addStreamSourceFile = useCallback(async (path) => { if (allFilesMeta[path]) return; diff --git a/src/Settings.jsx b/src/Settings.jsx index e540b89..b2b5c44 100644 --- a/src/Settings.jsx +++ b/src/Settings.jsx @@ -47,7 +47,7 @@ const Settings = memo(({ }) => { const { t } = useTranslation(); - const { customOutDir, changeOutDir, keyframeCut, toggleKeyframeCut, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, autoSaveProjectFile, setAutoSaveProjectFile, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, ffmpegExperimental, setFfmpegExperimental, hideNotifications, setHideNotifications, autoLoadTimecode, setAutoLoadTimecode, enableTransferTimestamps, setEnableTransferTimestamps, enableAutoHtml5ify, setEnableAutoHtml5ify, customFfPath, setCustomFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir } = useUserSettings(); + const { customOutDir, changeOutDir, keyframeCut, toggleKeyframeCut, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, autoSaveProjectFile, setAutoSaveProjectFile, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, ffmpegExperimental, setFfmpegExperimental, hideNotifications, setHideNotifications, autoLoadTimecode, setAutoLoadTimecode, enableTransferTimestamps, setEnableTransferTimestamps, enableAutoHtml5ify, setEnableAutoHtml5ify, customFfPath, setCustomFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, setEnableOverwriteOutput } = useUserSettings(); const onLangChange = useCallback((e) => { const { value } = e.target; @@ -271,6 +271,17 @@ const Settings = memo(({ + + {t('Overwrite files when exporting, if a file with the same name as the output file name exists?')} + + setEnableOverwriteOutput(e.target.checked)} + /> + + + {t('Auto load timecode from file as an offset in the timeline?')} diff --git a/src/dialogs.jsx b/src/dialogs.jsx index 720a6f8..0c7c9c4 100644 --- a/src/dialogs.jsx +++ b/src/dialogs.jsx @@ -169,6 +169,13 @@ export async function showDiskFull() { }); } +export async function showRefuseToOverwrite() { + await Swal.fire({ + icon: 'warning', + text: i18n.t('Output file already exists, refusing to overwrite. You can turn on overwriting in settings.'), + }); +} + export async function askForImportChapters() { const { value } = await Swal.fire({ icon: 'question', diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 4f5bd54..bfd20ad 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -1,5 +1,4 @@ import pMap from 'p-map'; -import flatMap from 'lodash/flatMap'; import sortBy from 'lodash/sortBy'; import moment from 'moment'; import i18n from 'i18next'; @@ -13,9 +12,13 @@ const { join } = window.require('path'); const FileType = window.require('file-type'); const readline = window.require('readline'); const isDev = window.require('electron-is-dev'); +const { pathExists } = window.require('fs-extra'); let customFfPath; + +export class RefuseOverwriteError extends Error {} + // Note that this does not work on MAS because of sandbox restrictions export function setCustomFfPath(path) { customFfPath = path; @@ -325,14 +328,21 @@ function getPreferredCodecFormat({ codec_name: codec, codec_type: type }) { return undefined; } -async function extractNonAttachmentStreams({ customOutDir, filePath, streams }) { +async function extractNonAttachmentStreams({ customOutDir, filePath, streams, enableOverwriteOutput }) { if (streams.length === 0) return; console.log('Extracting', streams.length, 'normal streams'); - const streamArgs = flatMap(streams, ({ index, codec, type, format: { format, ext } }) => [ - '-map', `0:${index}`, '-c', 'copy', '-f', format, '-y', getOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` }), - ]); + let streamArgs = []; + await pMap(streams, async ({ index, codec, type, format: { format, ext } }) => { + const outPath = getOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` }); + if (!enableOverwriteOutput && await pathExists(outPath)) throw new RefuseOverwriteError(); + + streamArgs = [ + ...streamArgs, + '-map', `0:${index}`, '-c', 'copy', '-f', format, '-y', outPath, + ]; + }, { concurrency: 1 }); const ffmpegArgs = [ '-hide_banner', @@ -345,17 +355,22 @@ async function extractNonAttachmentStreams({ customOutDir, filePath, streams }) console.log(stdout); } -async function extractAttachmentStreams({ customOutDir, filePath, streams }) { +async function extractAttachmentStreams({ customOutDir, filePath, streams, enableOverwriteOutput }) { if (streams.length === 0) return; console.log('Extracting', streams.length, 'attachment streams'); - const streamArgs = flatMap(streams, ({ index, codec_name: codec, codec_type: type }) => { + let streamArgs = []; + await pMap(streams, async ({ index, codec_name: codec, codec_type: type }) => { const ext = codec || 'bin'; - return [ - `-dump_attachment:${index}`, getOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` }), + const outPath = getOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` }); + if (!enableOverwriteOutput && await pathExists(outPath)) throw new RefuseOverwriteError(); + + streamArgs = [ + ...streamArgs, + `-dump_attachment:${index}`, outPath, ]; - }); + }, { concurrency: 1 }); const ffmpegArgs = [ '-y', @@ -377,7 +392,7 @@ async function extractAttachmentStreams({ customOutDir, filePath, streams }) { } // https://stackoverflow.com/questions/32922226/extract-every-audio-and-subtitles-from-a-video-with-ffmpeg -export async function extractStreams({ filePath, customOutDir, streams }) { +export async function extractStreams({ filePath, customOutDir, streams, enableOverwriteOutput }) { const attachmentStreams = streams.filter((s) => s.codec_type === 'attachment'); const nonAttachmentStreams = streams.filter((s) => s.codec_type !== 'attachment'); @@ -394,8 +409,8 @@ export async function extractStreams({ filePath, customOutDir, streams }) { // TODO progress // Attachment streams are handled differently from normal streams - await extractNonAttachmentStreams({ customOutDir, filePath, streams: outStreams }); - await extractAttachmentStreams({ customOutDir, filePath, streams: attachmentStreams }); + await extractNonAttachmentStreams({ customOutDir, filePath, streams: outStreams, enableOverwriteOutput }); + await extractAttachmentStreams({ customOutDir, filePath, streams: attachmentStreams, enableOverwriteOutput }); } async function renderThumbnail(filePath, timestamp) { diff --git a/src/hooks/useFfmpegOperations.js b/src/hooks/useFfmpegOperations.js index b9e4f61..d6219a9 100644 --- a/src/hooks/useFfmpegOperations.js +++ b/src/hooks/useFfmpegOperations.js @@ -4,7 +4,7 @@ import sum from 'lodash/sum'; import pMap from 'p-map'; import { getOutPath, transferTimestamps, getOutFileExtension, getOutDir, deleteDispositionValue, getHtml5ifiedPath } from '../util'; -import { isCuttingStart, isCuttingEnd, handleProgress, getFfCommandLine, getFfmpegPath, getDuration, runFfmpeg, createChaptersFromSegments, readFileMeta, cutEncodeSmartPart, getExperimentalArgs, html5ify as ffmpegHtml5ify, getVideoTimescaleArgs } from '../ffmpeg'; +import { isCuttingStart, isCuttingEnd, handleProgress, getFfCommandLine, getFfmpegPath, getDuration, runFfmpeg, createChaptersFromSegments, readFileMeta, cutEncodeSmartPart, getExperimentalArgs, html5ify as ffmpegHtml5ify, getVideoTimescaleArgs, RefuseOverwriteError } from '../ffmpeg'; import { getMapStreamsArgs, getStreamIdsToCopy } from '../util/streams'; import { getSmartCutParams } from '../smartcut'; @@ -326,6 +326,7 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps }) { onProgress: onTotalProgress, keyframeCut, copyFileStreams, allFilesMeta, outFormat, appendFfmpegCommandLog, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, avoidNegativeTs, customTagsByFile, customTagsByStreamId, dispositionByStreamId, chapters, preserveMetadataOnMerge, enableSmartCut, + enableOverwriteOutput, }) => { console.log('customTagsByFile', customTagsByFile); console.log('customTagsByStreamId', customTagsByStreamId); @@ -338,6 +339,10 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps }) { const chaptersPath = await writeChaptersFfmetadata(outputDir, chapters); + async function checkOverwrite(path) { + if (!enableOverwriteOutput && await fs.pathExists(path)) throw new RefuseOverwriteError(); + } + // This function will either call cutSingle (if no smart cut enabled) // or if enabled, will first cut&encode the part before the next keyframe, trying to match the input file's codec params @@ -348,6 +353,7 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps }) { if (!enableSmartCut) { // old fashioned way const outPath = getSegmentOutPath(); + await checkOverwrite(outPath); await cutSingle({ cutFrom: desiredCutFrom, cutTo, chaptersPath, outPath, copyFileStreams, keyframeCut, avoidNegativeTs, videoDuration, rotation, allFilesMeta, outFormat, appendFfmpegCommandLog, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, customTagsByFile, customTagsByStreamId, dispositionByStreamId, onProgress: (progress) => onSingleProgress(i, progress), }); @@ -378,6 +384,8 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps }) { ? getOutPath({ customOutDir, filePath, nameSuffix: `smartcut-segment-copy-${i}${ext}` }) : getSegmentOutPath(); + if (!needsSmartCut) await checkOverwrite(smartCutMainPartOutPath); + const smartCutEncodedPartOutPath = getOutPath({ customOutDir, filePath, nameSuffix: `smartcut-segment-encode-${i}${ext}` }); const smartCutSegmentsToConcat = [smartCutEncodedPartOutPath, smartCutMainPartOutPath]; @@ -401,6 +409,7 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps }) { const { streams: streamsAfterCut } = await readFileMeta(smartCutMainPartOutPath); const outPath = getSegmentOutPath(); + await checkOverwrite(outPath); await concatFiles({ paths: smartCutSegmentsToConcat, outDir: outputDir, outPath, metadataFromPath: smartCutMainPartOutPath, outFormat, includeAllStreams: true, streams: streamsAfterCut, ffmpegExperimental, preserveMovData, movFastStart, chapters, preserveMetadataOnMerge, videoTimebase, appendFfmpegCommandLog, onProgress: onConcatProgress }); return outPath; diff --git a/src/hooks/useUserSettingsRoot.js b/src/hooks/useUserSettingsRoot.js index 01d5a85..2396590 100644 --- a/src/hooks/useUserSettingsRoot.js +++ b/src/hooks/useUserSettingsRoot.js @@ -113,6 +113,8 @@ export default () => { useEffect(() => safeSetConfig('customFfPath', customFfPath), [customFfPath]); const [storeProjectInWorkingDir, setStoreProjectInWorkingDir] = useState(safeGetConfigInitial('storeProjectInWorkingDir')); useEffect(() => safeSetConfig('storeProjectInWorkingDir', storeProjectInWorkingDir), [storeProjectInWorkingDir]); + const [enableOverwriteOutput, setEnableOverwriteOutput] = useState(safeGetConfigInitial('enableOverwriteOutput')); + useEffect(() => safeSetConfig('enableOverwriteOutput', enableOverwriteOutput), [enableOverwriteOutput]); const resetKeyBindings = useCallback(() => { configStore.reset('keyBindings'); @@ -205,5 +207,7 @@ export default () => { setCustomFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, + enableOverwriteOutput, + setEnableOverwriteOutput, }; };