show error also if resulting duplicates

fixes #894
pull/982/head
Mikael Finstad 4 years ago
parent 53384a8bb3
commit 6926df5f00
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -57,8 +57,8 @@ import { formatYouTube, getTimeFromFrameNum as getTimeFromFrameNumRaw, getFrameC
import {
getOutPath, toast, errorToast, handleError, setFileNameTitle, getOutDir, withBlur,
checkDirWriteAccess, dirExists, openDirToast, isMasBuild, isStoreBuild, dragPreventer, doesPlayerSupportFile,
isDurationValid, isWindows, filenamify, getOutFileExtension, generateSegFileName, defaultOutSegTemplate,
hasDuplicates, havePermissionToReadFile, isMac, resolvePathIfNeeded, pathExists, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile,
isDurationValid, filenamify, getOutFileExtension, generateSegFileName, defaultOutSegTemplate,
havePermissionToReadFile, resolvePathIfNeeded, pathExists, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile,
deleteFiles, isStreamThumbnail, getAudioStreams, getVideoStreams, isOutOfSpaceError, shuffleArray,
} from './util';
import { formatDuration } from './util/duration';
@ -66,14 +66,14 @@ import { adjustRate } from './util/rate-calculator';
import { askForOutDir, askForImportChapters, createNumSegments, createFixedDurationSegments, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, cleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, openAbout, showEditableJsonDialog } from './dialogs';
import { openSendReportDialog } from './reporting';
import { fallbackLng } from './i18n';
import { createSegment, getCleanCutSegments, getSegApparentStart, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags } from './segments';
import { createSegment, getCleanCutSegments, getSegApparentStart, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, getOutSegError as getOutSegErrorRaw } from './segments';
const isDev = window.require('electron-is-dev');
const electron = window.require('electron'); // eslint-disable-line
const { exists } = window.require('fs-extra');
const filePathToUrl = window.require('file-url');
const { extname, parse: parsePath, sep: pathSep, join: pathJoin, normalize: pathNormalize, basename, dirname } = window.require('path');
const { extname, parse: parsePath, join: pathJoin, basename, dirname } = window.require('path');
const { dialog } = electron.remote;
@ -1105,26 +1105,7 @@ const App = memo(() => {
})
), [segmentsToExport, filenamifyOrNot, isCustomFormatSelected, fileFormat, filePath, safeOutputFileName]);
// TODO improve user feedback
const isOutSegFileNamesValid = useCallback((fileNames) => fileNames.every((fileName) => {
if (!filePath) return false;
const invalidChars = [pathSep];
// Colon is invalid on windows https://github.com/mifi/lossless-cut/issues/631 and on MacOS, but not Linux https://github.com/mifi/lossless-cut/issues/830
if (isMac || isWindows) invalidChars.push(':');
const outPath = pathNormalize(pathJoin(outputDir, fileName));
const sameAsInputPath = outPath === pathNormalize(filePath);
const windowsMaxPathLength = 259;
const shouldCheckPathLength = isWindows || isDev;
return (
fileName.length > 0
&& !invalidChars.some((c) => fileName.includes(c))
&& !sameAsInputPath
&& (!shouldCheckPathLength || outPath.length < windowsMaxPathLength)
);
}), [outputDir, filePath]);
const getOutSegError = useCallback((fileNames) => getOutSegErrorRaw({ fileNames, filePath, outputDir }), [outputDir, filePath]);
const openSendReportDialogWithState = useCallback(async (err) => {
const state = {
@ -1170,7 +1151,7 @@ const App = memo(() => {
console.log('outSegTemplateOrDefault', outSegTemplateOrDefault);
let outSegFileNames = generateOutSegFileNames({ segments: segmentsToExport, template: outSegTemplateOrDefault });
if (!isOutSegFileNamesValid(outSegFileNames) || hasDuplicates(outSegFileNames)) {
if (getOutSegError(outSegFileNames) != null) {
console.error('Output segments file name invalid, using default instead', outSegFileNames);
outSegFileNames = generateOutSegFileNames({ segments: segmentsToExport, template: defaultOutSegTemplate });
}
@ -1252,7 +1233,7 @@ const App = memo(() => {
setWorking();
setCutProgress();
}
}, [numStreamsToCopy, setWorking, segmentsToChaptersOnly, enabledSegments, outSegTemplateOrDefault, generateOutSegFileNames, segmentsToExport, isOutSegFileNamesValid, cutMultiple, outputDir, fileFormat, duration, isRotationSet, effectiveRotation, copyFileStreams, keyframeCut, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, avoidNegativeTs, customTagsByFile, customTagsByStreamId, dispositionByStreamId, willMerge, fileFormatData, mainStreams, exportExtraStreams, hideAllNotifications, segmentsToChapters, invertCutSegments, autoMergeSegments, customOutDir, isCustomFormatSelected, autoDeleteMergedSegments, preserveMetadataOnMerge, filePath, nonCopiedExtraStreams, handleCutFailed]);
}, [numStreamsToCopy, setWorking, segmentsToChaptersOnly, enabledSegments, outSegTemplateOrDefault, generateOutSegFileNames, segmentsToExport, getOutSegError, cutMultiple, outputDir, fileFormat, duration, isRotationSet, effectiveRotation, copyFileStreams, keyframeCut, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, avoidNegativeTs, customTagsByFile, customTagsByStreamId, dispositionByStreamId, willMerge, fileFormatData, mainStreams, exportExtraStreams, hideAllNotifications, segmentsToChapters, invertCutSegments, autoMergeSegments, customOutDir, isCustomFormatSelected, autoDeleteMergedSegments, preserveMetadataOnMerge, filePath, nonCopiedExtraStreams, handleCutFailed]);
const onExportPress = useCallback(async () => {
if (!filePath || workingRef.current) return;
@ -2442,7 +2423,7 @@ const App = memo(() => {
/>
</SideSheet>
<ExportConfirm filePath={filePath} autoMerge={autoMerge} setAutoMerge={setAutoMerge} areWeCutting={areWeCutting} enabledSegments={enabledSegments} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} keyframeCut={keyframeCut} toggleKeyframeCut={toggleKeyframeCut} renderOutFmt={renderOutFmt} preserveMovData={preserveMovData} togglePreserveMovData={togglePreserveMovData} movFastStart={movFastStart} toggleMovFastStart={toggleMovFastStart} avoidNegativeTs={avoidNegativeTs} setAvoidNegativeTs={setAvoidNegativeTs} changeOutDir={changeOutDir} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} setStreamsSelectorShown={setStreamsSelectorShown} exportConfirmEnabled={exportConfirmEnabled} toggleExportConfirmEnabled={toggleExportConfirmEnabled} segmentsToChapters={segmentsToChapters} toggleSegmentsToChapters={toggleSegmentsToChapters} outFormat={fileFormat} preserveMetadataOnMerge={preserveMetadataOnMerge} togglePreserveMetadataOnMerge={togglePreserveMetadataOnMerge} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} isOutSegFileNamesValid={isOutSegFileNamesValid} autoDeleteMergedSegments={autoDeleteMergedSegments} setAutoDeleteMergedSegments={setAutoDeleteMergedSegments} safeOutputFileName={safeOutputFileName} toggleSafeOutputFileName={toggleSafeOutputFileName} segmentsToChaptersOnly={segmentsToChaptersOnly} setSegmentsToChaptersOnly={setSegmentsToChaptersOnly} />
<ExportConfirm filePath={filePath} autoMerge={autoMerge} setAutoMerge={setAutoMerge} areWeCutting={areWeCutting} enabledSegments={enabledSegments} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} keyframeCut={keyframeCut} toggleKeyframeCut={toggleKeyframeCut} renderOutFmt={renderOutFmt} preserveMovData={preserveMovData} togglePreserveMovData={togglePreserveMovData} movFastStart={movFastStart} toggleMovFastStart={toggleMovFastStart} avoidNegativeTs={avoidNegativeTs} setAvoidNegativeTs={setAvoidNegativeTs} changeOutDir={changeOutDir} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} setStreamsSelectorShown={setStreamsSelectorShown} exportConfirmEnabled={exportConfirmEnabled} toggleExportConfirmEnabled={toggleExportConfirmEnabled} segmentsToChapters={segmentsToChapters} toggleSegmentsToChapters={toggleSegmentsToChapters} outFormat={fileFormat} preserveMetadataOnMerge={preserveMetadataOnMerge} togglePreserveMetadataOnMerge={togglePreserveMetadataOnMerge} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} getOutSegError={getOutSegError} autoDeleteMergedSegments={autoDeleteMergedSegments} setAutoDeleteMergedSegments={setAutoDeleteMergedSegments} safeOutputFileName={safeOutputFileName} toggleSafeOutputFileName={toggleSafeOutputFileName} segmentsToChaptersOnly={segmentsToChaptersOnly} setSegmentsToChaptersOnly={setSegmentsToChaptersOnly} />
<HelpSheet
visible={helpVisible}

@ -44,7 +44,7 @@ const ExportConfirm = memo(({
changeOutDir, outputDir, numStreamsTotal, numStreamsToCopy, setStreamsSelectorShown,
exportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, toggleSegmentsToChapters, outFormat,
preserveMetadataOnMerge, togglePreserveMetadataOnMerge, outSegTemplate, setOutSegTemplate, generateOutSegFileNames,
filePath, currentSegIndexSafe, isOutSegFileNamesValid, autoDeleteMergedSegments, setAutoDeleteMergedSegments,
filePath, currentSegIndexSafe, getOutSegError, autoDeleteMergedSegments, setAutoDeleteMergedSegments,
safeOutputFileName, toggleSafeOutputFileName, segmentsToChaptersOnly, setSegmentsToChaptersOnly,
}) => {
const { t } = useTranslation();
@ -131,7 +131,7 @@ const ExportConfirm = memo(({
</li>
{canEditTemplate && (
<li>
<OutSegTemplateEditor filePath={filePath} helpIcon={outSegTemplateHelpIcon} outSegTemplate={outSegTemplate} setOutSegTemplate={setOutSegTemplate} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} isOutSegFileNamesValid={isOutSegFileNamesValid} safeOutputFileName={safeOutputFileName} toggleSafeOutputFileName={toggleSafeOutputFileName} />
<OutSegTemplateEditor filePath={filePath} helpIcon={outSegTemplateHelpIcon} outSegTemplate={outSegTemplate} setOutSegTemplate={setOutSegTemplate} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} getOutSegError={getOutSegError} safeOutputFileName={safeOutputFileName} toggleSafeOutputFileName={toggleSafeOutputFileName} />
</li>
)}
</ul>

@ -2,7 +2,7 @@ import React, { memo, useState, useEffect, useCallback } from 'react';
import { useDebounce } from 'use-debounce';
import i18n from 'i18next';
import { useTranslation } from 'react-i18next';
import { Button, Alert, IconButton, TickIcon, ResetIcon } from 'evergreen-ui';
import { Text, Button, Alert, IconButton, TickIcon, ResetIcon } from 'evergreen-ui';
import Swal from 'sweetalert2';
import withReactContent from 'sweetalert2-react-content';
@ -14,7 +14,7 @@ const ReactSwal = withReactContent(Swal);
const inputStyle = { flexGrow: 1, fontFamily: 'inherit', fontSize: '.8em' };
const OutSegTemplateEditor = memo(({ helpIcon, outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe, isOutSegFileNamesValid, safeOutputFileName, toggleSafeOutputFileName }) => {
const OutSegTemplateEditor = memo(({ helpIcon, outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe, getOutSegError, safeOutputFileName, toggleSafeOutputFileName }) => {
const [text, setText] = useState(outSegTemplate);
const [debouncedText] = useDebounce(text, 500);
const [validText, setValidText] = useState();
@ -30,9 +30,9 @@ const OutSegTemplateEditor = memo(({ helpIcon, outSegTemplate, setOutSegTemplate
try {
const generatedOutSegFileNames = generateOutSegFileNames({ template: debouncedText });
setOutSegFileNames(generatedOutSegFileNames);
const isOutSegTemplateValid = isOutSegFileNamesValid(generatedOutSegFileNames);
if (!isOutSegTemplateValid) {
setError(t('This template will result in invalid file names'));
const outSegError = getOutSegError(generatedOutSegFileNames);
if (outSegError) {
setError(outSegError);
setValidText();
return;
}
@ -44,9 +44,15 @@ const OutSegTemplateEditor = memo(({ helpIcon, outSegTemplate, setOutSegTemplate
setValidText();
setError(err.message);
}
}, [debouncedText, generateOutSegFileNames, isOutSegFileNamesValid, t]);
}, [debouncedText, generateOutSegFileNames, getOutSegError, t]);
const onAllSegmentsPreviewPress = () => ReactSwal.fire({ title: t('Resulting segment file names'), html: <div style={{ textAlign: 'left', overflowY: 'auto', maxHeight: 400 }}>{outSegFileNames.map((f) => <div key={f} style={{ marginBottom: 7 }}>{f}</div>)}</div> });
const onAllSegmentsPreviewPress = () => ReactSwal.fire({
title: t('Resulting segment file names'),
html: (
<div style={{ textAlign: 'left', overflowY: 'auto', maxHeight: 400 }}>
{outSegFileNames.map((f) => <div key={f} style={{ marginBottom: 7 }}>{f}</div>)}
</div>
) });
useEffect(() => {
if (validText != null) setOutSegTemplate(validText);
@ -84,7 +90,7 @@ const OutSegTemplateEditor = memo(({ helpIcon, outSegTemplate, setOutSegTemplate
<IconButton title={t('Close')} icon={TickIcon} height={20} onClick={onToggleClick} marginLeft={5} intent="success" />
</div>
<div>
{error != null && <Alert intent="danger" appearance="card">{`${i18n.t('There is an error in the file name template:')} ${error}`}</Alert>}
{error != null && <Alert intent="danger" appearance="card"><Text>{i18n.t('There is an error in the file name template:')}</Text><br /><Text>{error}</Text></Alert>}
{/* eslint-disable-next-line no-template-curly-in-string */}
<div style={{ fontSize: '.8em', color: 'rgba(255,255,255,0.7)' }}>{`${i18n.t('Variables')}`} {'${FILENAME} ${CUT_FROM} ${CUT_TO} ${SEG_NUM} ${SEG_LABEL} ${SEG_SUFFIX} ${EXT} ${SEG_TAGS.XX}'}</div>
</div>

@ -1,5 +1,11 @@
import { v4 as uuidv4 } from 'uuid';
import sortBy from 'lodash/sortBy';
import i18n from 'i18next';
import { isMac, isWindows, hasDuplicates } from './util';
const { sep: pathSep, join: pathJoin, normalize: pathNormalize } = window.require('path');
const isDev = window.require('electron-is-dev');
export const createSegment = ({ start, end, name, tags, segIndex } = {}) => ({
start,
@ -80,3 +86,45 @@ export function invertSegments(sortedCutSegments, duration) {
// https://github.com/mifi/lossless-cut/issues/909
return ret.filter(({ start, end }) => end == null || start == null || end > start);
}
export function getOutSegError({ fileNames, filePath, outputDir }) {
if (hasDuplicates(fileNames)) return i18n.t('Template results in duplicate file names');
let error;
// eslint-disable-next-line no-restricted-syntax
for (const fileName of fileNames) {
if (!filePath) {
error = 'No file path';
break;
}
const invalidChars = [pathSep];
// Colon is invalid on windows https://github.com/mifi/lossless-cut/issues/631 and on MacOS, but not Linux https://github.com/mifi/lossless-cut/issues/830
if (isMac || isWindows) invalidChars.push(':');
const outPath = pathNormalize(pathJoin(outputDir, fileName));
const sameAsInputPath = outPath === pathNormalize(filePath);
const windowsMaxPathLength = 259;
const shouldCheckPathLength = isWindows || isDev;
if (fileName.length === 0) {
error = i18n.t('At least one resulting file name has no length');
break;
}
if (invalidChars.some((c) => fileName.includes(c))) {
error = i18n.t('At least one resulting file name contains invalid characters');
break;
}
if (sameAsInputPath) {
error = i18n.t('At least one resulting file name is the same as the input path');
break;
}
if (shouldCheckPathLength && outPath.length >= windowsMaxPathLength) {
error = i18n.t('At least one resulting file will have a too long path');
break;
}
}
return error;
}

Loading…
Cancel
Save