1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-22 10:22:31 +01:00

concat improvements

- when merging files, the output file name will always contain a unique suffix to prevent overwriting any existing files
- the UI will allow the user to enter a custom output file name
- rename getOutPath
This commit is contained in:
Mikael Finstad 2022-09-11 22:53:00 +02:00
parent fc93662254
commit ceda857d6d
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
6 changed files with 64 additions and 35 deletions

View File

@ -62,7 +62,7 @@ import { shouldCopyStreamByDefault, getAudioStreams, getRealVideoStreams, defaul
import { exportEdlFile, readEdlFile, saveLlcProject, loadLlcProject, askForEdlImport } from './edlStore';
import { formatYouTube, getFrameCountRaw } from './edlFormats';
import {
getOutPath, toast, errorToast, handleError, setFileNameTitle, getOutDir, getFileDir,
getOutPath, getSuffixedOutPath, toast, errorToast, handleError, setFileNameTitle, getOutDir, getFileDir,
checkDirWriteAccess, dirExists, openDirToast, isMasBuild, isStoreBuild, dragPreventer,
isDurationValid, filenamify, getOutFileExtension, generateSegFileName, defaultOutSegTemplate,
havePermissionToReadFile, resolvePathIfNeeded, getPathReadAccessError, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile,
@ -583,9 +583,9 @@ const App = memo(() => {
const projectSuffix = 'proj.llc';
const oldProjectSuffix = 'llc-edl.csv';
// New LLC format can be stored along with input file or in working dir (customOutDir)
const getEdlFilePath = useCallback((fp, storeProjectInWorkingDir2 = false) => getOutPath({ customOutDir: storeProjectInWorkingDir2 ? customOutDir : undefined, filePath: fp, nameSuffix: projectSuffix }), [customOutDir]);
const getEdlFilePath = useCallback((fp, storeProjectInWorkingDir2 = false) => getSuffixedOutPath({ customOutDir: storeProjectInWorkingDir2 ? customOutDir : undefined, filePath: fp, nameSuffix: projectSuffix }), [customOutDir]);
// Old versions of LosslessCut used CSV files and stored them in customOutDir:
const getEdlFilePathOld = useCallback((fp) => getOutPath({ customOutDir, filePath: fp, nameSuffix: oldProjectSuffix }), [customOutDir]);
const getEdlFilePathOld = useCallback((fp) => getSuffixedOutPath({ customOutDir, filePath: fp, nameSuffix: oldProjectSuffix }), [customOutDir]);
const projectFileSavePath = useMemo(() => getEdlFilePath(filePath, storeProjectInWorkingDir), [getEdlFilePath, filePath, storeProjectInWorkingDir]);
const currentSaveOperation = useMemo(() => {
@ -960,7 +960,7 @@ const App = memo(() => {
async function doHtml5ify() {
if (speed == null) return undefined;
if (speed === 'fastest') {
const path = getOutPath({ customOutDir: cod, filePath: fp, nameSuffix: `${html5ifiedPrefix}${html5dummySuffix}.mkv` });
const path = getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: `${html5ifiedPrefix}${html5dummySuffix}.mkv` });
try {
setCutProgress(0);
await html5ifyDummy({ filePath: fp, outPath: path, onProgress: setCutProgress });
@ -1094,19 +1094,21 @@ const App = memo(() => {
});
}, []);
const userConcatFiles = useCallback(async ({ paths, includeAllStreams, streams, fileFormat: outFormat, isCustomFormatSelected: isCustomFormatSelected2, clearBatchFilesAfterConcat }) => {
const userConcatFiles = useCallback(async ({ paths, includeAllStreams, streams, fileFormat: outFormat, fileName: outFileName, clearBatchFilesAfterConcat }) => {
if (workingRef.current) return;
try {
setConcatDialogVisible(false);
setWorking(i18n.t('Merging'));
const firstPath = paths[0];
if (!firstPath) return;
const { newCustomOutDir, cancel } = await ensureAccessibleDirectories({ inputPath: firstPath });
if (cancel) return;
const ext = getOutFileExtension({ isCustomFormatSelected: isCustomFormatSelected2, outFormat, filePath: firstPath });
const outPath = getOutPath({ customOutDir: newCustomOutDir, filePath: firstPath, nameSuffix: `merged${ext}` });
const outDir = getOutDir(customOutDir, firstPath);
const outDir = getOutDir(newCustomOutDir, firstPath);
const outPath = getOutPath({ customOutDir: newCustomOutDir, filePath: firstPath, fileName: outFileName });
let chaptersFromSegments;
if (segmentsToChapters) {
@ -1130,7 +1132,7 @@ const App = memo(() => {
setWorking();
setCutProgress();
}
}, [setWorking, ensureAccessibleDirectories, customOutDir, segmentsToChapters, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch]);
}, [setWorking, ensureAccessibleDirectories, segmentsToChapters, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch]);
const cleanupFilesDialog = useCallback(async () => {
if (!isFileOpened) return;

View File

@ -1,6 +1,6 @@
import strongDataUri from 'strong-data-uri';
import { getOutPath, transferTimestamps } from './util';
import { getSuffixedOutPath, transferTimestamps } from './util';
import { formatDuration } from './util/duration';
import { captureFrame as ffmpegCaptureFrame } from './ffmpeg';
@ -29,7 +29,7 @@ export async function captureFramesFfmpeg({ customOutDir, filePath, fromTime, ca
} else {
nameSuffix = `${time}.${captureFormat}`;
}
const outPath = getOutPath({ customOutDir, filePath, nameSuffix });
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix });
await ffmpegCaptureFrame({ timestamp: fromTime, videoPath: filePath, outPath, numFrames });
if (enableTransferTimestamps && numFrames === 1) await transferTimestamps(filePath, outPath, fromTime);
@ -42,7 +42,7 @@ export async function captureFrameFromTag({ customOutDir, filePath, currentTime,
const ext = mime.extension(buf.mimetype);
const time = formatDuration({ seconds: currentTime, fileNameFriendly: true });
const outPath = getOutPath({ customOutDir, filePath, nameSuffix: `${time}.${ext}` });
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `${time}.${ext}` });
await fs.writeFile(outPath, buf);
if (enableTransferTimestamps) await transferTimestamps(filePath, outPath, currentTime);

View File

@ -1,6 +1,6 @@
import React, { memo, useState, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { IconButton, Alert, Checkbox, Dialog, Button, Paragraph } from 'evergreen-ui';
import { TextInput, IconButton, Alert, Checkbox, Dialog, Button, Paragraph } from 'evergreen-ui';
import { AiOutlineMergeCells } from 'react-icons/ai';
import { FaQuestionCircle, FaCheckCircle, FaExclamationTriangle } from 'react-icons/fa';
import i18n from 'i18next';
@ -12,6 +12,7 @@ import useFileFormatState from '../hooks/useFileFormatState';
import OutputFormatSelect from './OutputFormatSelect';
import useUserSettings from '../hooks/useUserSettings';
import { isMov } from '../util/streams';
import { getOutFileExtension, getFileBaseName } from '../util';
const { basename } = window.require('path');
@ -36,6 +37,8 @@ const ConcatDialog = memo(({
const [clearBatchFilesAfterConcat, setClearBatchFilesAfterConcat] = useState(false);
const [settingsVisible, setSettingsVisible] = useState(false);
const [enableReadFileMeta, setEnableReadFileMeta] = useState(false);
const [outFileName, setOutFileName] = useState();
const [uniqueSuffix, setUniqueSuffix] = useState();
const { fileFormat, setFileFormat, detectedFileFormat, setDetectedFileFormat, isCustomFormatSelected } = useFileFormatState();
@ -53,12 +56,14 @@ const ConcatDialog = memo(({
setFileMeta();
setFileFormat();
setDetectedFileFormat();
setOutFileName();
const fileMetaNew = await readFileMeta(firstPath);
const fileFormatNew = await getSmarterOutFormat({ filePath: firstPath, fileMeta: fileMetaNew });
if (aborted) return;
setFileMeta(fileMetaNew);
setFileFormat(fileFormatNew);
setDetectedFileFormat(fileFormatNew);
setUniqueSuffix(new Date().getTime());
})().catch(console.error);
return () => {
@ -66,12 +71,23 @@ const ConcatDialog = memo(({
};
}, [firstPath, isShown, setDetectedFileFormat, setFileFormat]);
useEffect(() => {
if (fileFormat == null || firstPath == null) {
setOutFileName();
return;
}
const ext = getOutFileExtension({ isCustomFormatSelected, outFormat: fileFormat, filePath: firstPath });
setOutFileName(`${getFileBaseName(firstPath)}-merged-${uniqueSuffix}${ext}`);
}, [fileFormat, firstPath, isCustomFormatSelected, uniqueSuffix]);
const allFilesMeta = useMemo(() => {
if (paths.length === 0) return undefined;
const filtered = paths.map((path) => [path, allFilesMetaCache[path]]).filter(([, it]) => it);
return filtered.length === paths.length ? filtered : undefined;
}, [allFilesMetaCache, paths]);
const isOutFileNameValid = outFileName != null && outFileName.length > 0;
const problemsByFile = useMemo(() => {
if (!allFilesMeta) return [];
const allFilesMetaExceptFirstFile = allFilesMeta.slice(1);
@ -136,7 +152,7 @@ const ConcatDialog = memo(({
const onOutputFormatUserChange = useCallback((newFormat) => setFileFormat(newFormat), [setFileFormat]);
const onConcatClick = useCallback(() => onConcat({ paths, includeAllStreams, streams: fileMeta.streams, fileFormat, isCustomFormatSelected, clearBatchFilesAfterConcat }), [clearBatchFilesAfterConcat, fileFormat, fileMeta, includeAllStreams, isCustomFormatSelected, onConcat, paths]);
const onConcatClick = useCallback(() => onConcat({ paths, includeAllStreams, streams: fileMeta.streams, fileName: outFileName, fileFormat, clearBatchFilesAfterConcat }), [clearBatchFilesAfterConcat, fileFormat, fileMeta, includeAllStreams, onConcat, outFileName, paths]);
return (
<>
@ -147,16 +163,22 @@ const ConcatDialog = memo(({
topOffset="3vh"
width="90vw"
footer={(
<div style={{ display: 'flex', alignItems: 'center' }}>
<Checkbox checked={enableReadFileMeta} onChange={(e) => setEnableReadFileMeta(e.target.checked)} label={t('Check files')} marginLeft={10} marginRight={10} />
<Button iconBefore={FaCheckCircle} onClick={() => setSettingsVisible(true)}>{t('Options')}</Button>
{fileFormat && detectedFileFormat ? (
<OutputFormatSelect style={{ maxWidth: 180 }} detectedFileFormat={detectedFileFormat} fileFormat={fileFormat} onOutputFormatUserChange={onOutputFormatUserChange} />
) : (
<Button disabled isLoading>{t('Loading')}</Button>
)}
<Button iconBefore={<AiOutlineMergeCells />} isLoading={detectedFileFormat == null} appearance="primary" onClick={onConcatClick}>{t('Merge!')}</Button>
</div>
<>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'flex-end' }}>
<Checkbox checked={enableReadFileMeta} onChange={(e) => setEnableReadFileMeta(e.target.checked)} label={t('Check files')} marginLeft={10} marginRight={10} />
<Button iconBefore={FaCheckCircle} onClick={() => setSettingsVisible(true)}>{t('Options')}</Button>
{fileFormat && detectedFileFormat ? (
<OutputFormatSelect style={{ maxWidth: 180 }} detectedFileFormat={detectedFileFormat} fileFormat={fileFormat} onOutputFormatUserChange={onOutputFormatUserChange} />
) : (
<Button disabled isLoading>{t('Loading')}</Button>
)}
<Button iconBefore={<AiOutlineMergeCells />} isLoading={detectedFileFormat == null} disabled={!isOutFileNameValid} appearance="primary" onClick={onConcatClick}>{t('Merge!')}</Button>
</div>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'flex-end' }}>
<Paragraph marginRight=".5em">{t('Output file name')}:</Paragraph>
<TextInput value={outFileName || ''} onChange={(e) => setOutFileName(e.target.value)} />
</div>
</>
)}
>
<div style={containerStyle}>

View File

@ -5,7 +5,7 @@ import i18n from 'i18next';
import Timecode from 'smpte-timecode';
import { pcmAudioCodecs, getMapStreamsArgs } from './util/streams';
import { getOutPath, isDurationValid, getExtensionForFormat, isWindows, isMac, platform, arch } from './util';
import { getSuffixedOutPath, isDurationValid, getExtensionForFormat, isWindows, isMac, platform, arch } from './util';
const execa = window.require('execa');
const { join } = window.require('path');
@ -335,7 +335,7 @@ async function extractNonAttachmentStreams({ customOutDir, filePath, streams, en
let streamArgs = [];
await pMap(streams, async ({ index, codec, type, format: { format, ext } }) => {
const outPath = getOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` });
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` });
if (!enableOverwriteOutput && await pathExists(outPath)) throw new RefuseOverwriteError();
streamArgs = [
@ -363,7 +363,7 @@ async function extractAttachmentStreams({ customOutDir, filePath, streams, enabl
let streamArgs = [];
await pMap(streams, async ({ index, codec_name: codec, codec_type: type }) => {
const ext = codec || 'bin';
const outPath = getOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` });
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` });
if (!enableOverwriteOutput && await pathExists(outPath)) throw new RefuseOverwriteError();
streamArgs = [

View File

@ -3,7 +3,7 @@ import flatMap from 'lodash/flatMap';
import sum from 'lodash/sum';
import pMap from 'p-map';
import { getOutPath, transferTimestamps, getOutFileExtension, getOutDir, deleteDispositionValue, getHtml5ifiedPath } from '../util';
import { getSuffixedOutPath, transferTimestamps, getOutFileExtension, getOutDir, deleteDispositionValue, getHtml5ifiedPath } from '../util';
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';
@ -381,12 +381,12 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps }) {
const ext = getOutFileExtension({ isCustomFormatSelected: true, outFormat, filePath });
const smartCutMainPartOutPath = needsSmartCut
? getOutPath({ customOutDir, filePath, nameSuffix: `smartcut-segment-copy-${i}${ext}` })
? getSuffixedOutPath({ 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 smartCutEncodedPartOutPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `smartcut-segment-encode-${i}${ext}` });
const smartCutSegmentsToConcat = [smartCutEncodedPartOutPath, smartCutMainPartOutPath];
@ -429,7 +429,7 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps }) {
const autoConcatCutSegments = useCallback(async ({ customOutDir, isCustomFormatSelected, outFormat, segmentPaths, ffmpegExperimental, onProgress, preserveMovData, movFastStart, autoDeleteMergedSegments, chapterNames, preserveMetadataOnMerge, appendFfmpegCommandLog }) => {
const ext = getOutFileExtension({ isCustomFormatSelected, outFormat, filePath });
const outPath = getOutPath({ customOutDir, filePath, nameSuffix: `cut-merged-${new Date().getTime()}${ext}` });
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `cut-merged-${new Date().getTime()}${ext}` });
const outDir = getOutDir(customOutDir, filePath);
const chapters = await createChaptersFromSegments({ segmentPaths, chapterNames });
@ -477,7 +477,7 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps }) {
// https://stackoverflow.com/questions/34118013/how-to-determine-webm-duration-using-ffprobe
const fixInvalidDuration = useCallback(async ({ fileFormat, customOutDir }) => {
const ext = getOutFileExtension({ outFormat: fileFormat, filePath });
const outPath = getOutPath({ customOutDir, filePath, nameSuffix: `reformatted${ext}` });
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `reformatted${ext}` });
const ffmpegArgs = [
'-hide_banner',

View File

@ -27,9 +27,14 @@ export function getFileBaseName(filePath) {
return parsed.name;
}
export function getOutPath({ customOutDir, filePath, nameSuffix }) {
export function getOutPath({ customOutDir, filePath, fileName }) {
if (!filePath) return undefined;
return join(getOutDir(customOutDir, filePath), `${getFileBaseName(filePath)}-${nameSuffix}`);
return join(getOutDir(customOutDir, filePath), fileName);
}
export function getSuffixedOutPath({ customOutDir, filePath, nameSuffix }) {
if (!filePath) return undefined;
return getOutPath({ customOutDir, filePath, fileName: `${getFileBaseName(filePath)}-${nameSuffix}` });
}
export async function havePermissionToReadFile(filePath) {
@ -246,7 +251,7 @@ export async function findExistingHtml5FriendlyFile(fp, cod) {
export function getHtml5ifiedPath(cod, fp, type) {
// See also inside ffmpegHtml5ify
const ext = (isMac && ['slowest', 'slow', 'slow-audio'].includes(type)) ? 'mp4' : 'mkv';
return getOutPath({ customOutDir: cod, filePath: fp, nameSuffix: `${html5ifiedPrefix}${type}.${ext}` });
return getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: `${html5ifiedPrefix}${type}.${ext}` });
}
export async function deleteFiles({ toDelete, paths: { previewFilePath, sourceFilePath, projectFilePath } }) {