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:
parent
fc93662254
commit
ceda857d6d
20
src/App.jsx
20
src/App.jsx
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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}>
|
||||
|
@ -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 = [
|
||||
|
@ -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',
|
||||
|
11
src/util.js
11
src/util.js
@ -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 } }) {
|
||||
|
Loading…
Reference in New Issue
Block a user