kopia lustrzana https://github.com/mifi/lossless-cut
improvements
- fallback sanitize properly (force filenamify) - improve export failed feedback #1409 - add more invalid chars to filename check - always show out seg error in export pagepull/1413/head
rodzic
55e9a0a088
commit
967fe22cbe
24
src/App.jsx
24
src/App.jsx
|
@ -69,7 +69,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, showRefuseToOverwrite, showParametersDialog, openDirToast, openCutFinishedToast } from './dialogs';
|
||||
import { askForOutDir, askForInputDir, askForImportChapters, createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, openAbout, showEditableJsonDialog, askForShiftSegments, selectSegmentsByLabelDialog, confirmExtractFramesAsImages, showRefuseToOverwrite, showParametersDialog, openDirToast, openCutFinishedToast } from './dialogs';
|
||||
import { openSendReportDialog } from './reporting';
|
||||
import { fallbackLng } from './i18n';
|
||||
import { createSegment, getCleanCutSegments, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, convertSegmentsToChapters, hasAnySegmentOverlap, combineOverlappingSegments as combineOverlappingSegments2, isDurationValid } from './segments';
|
||||
|
@ -1178,8 +1178,6 @@ const App = memo(() => {
|
|||
const selectOnlyCurrentSegment = useCallback(() => selectOnlySegment(currentCutSeg), [currentCutSeg, selectOnlySegment]);
|
||||
const toggleCurrentSegmentSelected = useCallback(() => toggleSegmentSelected(currentCutSeg), [currentCutSeg, toggleSegmentSelected]);
|
||||
|
||||
const filenamifyOrNot = useCallback((name) => (safeOutputFileName ? filenamify(name) : name).substr(0, maxLabelLength), [safeOutputFileName, maxLabelLength]);
|
||||
|
||||
const onLabelSegment = useCallback(async (index) => {
|
||||
const { name } = cutSegments[index];
|
||||
const value = await labelSegmentDialog({ currentName: name, maxLength: maxLabelLength });
|
||||
|
@ -1205,13 +1203,15 @@ const App = memo(() => {
|
|||
|
||||
const areWeCutting = useMemo(() => segmentsToExport.some(({ start, end }) => isCuttingStart(start) || isCuttingEnd(end, duration)), [duration, segmentsToExport]);
|
||||
|
||||
const generateOutSegFileNames = useCallback(({ segments = segmentsToExport, template }) => (
|
||||
const generateOutSegFileNames = useCallback(({ segments = segmentsToExport, template, forceSafeOutputFileName }) => (
|
||||
segments.map((segment, i) => {
|
||||
const { start, end, name = '' } = segment;
|
||||
const cutFromStr = formatDuration({ seconds: start, fileNameFriendly: true });
|
||||
const cutToStr = formatDuration({ seconds: end, fileNameFriendly: true });
|
||||
const segNum = i + 1;
|
||||
|
||||
const filenamifyOrNot = (fileName) => (safeOutputFileName || forceSafeOutputFileName ? filenamify(fileName) : fileName).substr(0, maxLabelLength);
|
||||
|
||||
// https://github.com/mifi/lossless-cut/issues/583
|
||||
let segSuffix = '';
|
||||
if (name) segSuffix = `-${filenamifyOrNot(name)}`;
|
||||
|
@ -1228,7 +1228,7 @@ const App = memo(() => {
|
|||
const generated = generateSegFileName({ template, segSuffix, inputFileNameWithoutExt: fileNameWithoutExt, ext, segNum, segLabel: nameSanitized, cutFrom: cutFromStr, cutTo: cutToStr, tags: tagsSanitized });
|
||||
return safeOutputFileName ? generated.substring(0, 200) : generated; // If sanitation is enabled, make sure filename is not too long
|
||||
})
|
||||
), [segmentsToExport, filenamifyOrNot, isCustomFormatSelected, fileFormat, filePath, safeOutputFileName]);
|
||||
), [segmentsToExport, isCustomFormatSelected, fileFormat, filePath, safeOutputFileName, maxLabelLength]);
|
||||
|
||||
const getOutSegError = useCallback((fileNames) => getOutSegErrorRaw({ fileNames, filePath, outputDir }), [outputDir, filePath]);
|
||||
|
||||
|
@ -1250,10 +1250,10 @@ const App = memo(() => {
|
|||
openSendReportDialog(err, state);
|
||||
}, [filePath, fileFormat, externalFilesMeta, mainStreams, copyStreamIdsByFile, cutSegments, mainFileFormatData, rotation, shortestFlag, effectiveExportMode, outSegTemplate]);
|
||||
|
||||
const handleCutFailed = useCallback(async (err) => {
|
||||
const sendErrorReport = await showCutFailedDialog({ detectedFileFormat });
|
||||
const handleExportFailed = useCallback(async (err) => {
|
||||
const sendErrorReport = await showExportFailedDialog({ detectedFileFormat, safeOutputFileName });
|
||||
if (sendErrorReport) openSendReportDialogWithState(err);
|
||||
}, [openSendReportDialogWithState, detectedFileFormat]);
|
||||
}, [detectedFileFormat, safeOutputFileName, openSendReportDialogWithState]);
|
||||
|
||||
const closeExportConfirm = useCallback(() => setExportConfirmVisible(false), []);
|
||||
|
||||
|
@ -1287,8 +1287,8 @@ const App = memo(() => {
|
|||
|
||||
let outSegFileNames = generateOutSegFileNames({ segments: segmentsToExport, template: outSegTemplateOrDefault });
|
||||
if (getOutSegError(outSegFileNames) != null) {
|
||||
console.error('Output segments file name invalid, using default instead', outSegFileNames);
|
||||
outSegFileNames = generateOutSegFileNames({ segments: segmentsToExport, template: defaultOutSegTemplate });
|
||||
console.warn('Output segments file name invalid, using default instead', outSegFileNames);
|
||||
outSegFileNames = generateOutSegFileNames({ segments: segmentsToExport, template: defaultOutSegTemplate, forceSafeOutputFileName: true });
|
||||
}
|
||||
|
||||
// throw (() => { const err = new Error('test'); err.code = 'ENOENT'; return err; })();
|
||||
|
@ -1377,7 +1377,7 @@ const App = memo(() => {
|
|||
showDiskFull();
|
||||
return;
|
||||
}
|
||||
handleCutFailed(err);
|
||||
handleExportFailed(err);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1386,7 +1386,7 @@ const App = memo(() => {
|
|||
setWorking();
|
||||
setCutProgress();
|
||||
}
|
||||
}, [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]);
|
||||
}, [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, handleExportFailed]);
|
||||
|
||||
const onExportPress = useCallback(async () => {
|
||||
if (!filePath || workingRef.current || segmentsToExport.length < 1) return;
|
||||
|
|
|
@ -81,16 +81,18 @@ const OutSegTemplateEditor = memo(({ helpIcon, outSegTemplate, setOutSegTemplate
|
|||
|
||||
const onTextChange = useCallback((e) => setText(e.target.value), []);
|
||||
|
||||
const needToShow = shown || error != null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<span role="button" onClick={onShowClick} style={{ cursor: shown ? undefined : 'pointer' }}>
|
||||
<span role="button" onClick={onShowClick} style={{ cursor: needToShow ? undefined : 'pointer' }}>
|
||||
{t('Output name(s):')} {outSegFileNames != null && <HighlightedText style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{outSegFileNames[currentSegIndexSafe] || outSegFileNames[0]}</HighlightedText>}
|
||||
</span>
|
||||
{helpIcon}
|
||||
</div>
|
||||
|
||||
{shown && (
|
||||
{needToShow && (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 5, marginTop: 5 }}>
|
||||
<input type="text" style={inputStyle} onChange={onTextChange} value={text} autoComplete="off" autoCapitalize="off" autoCorrect="off" />
|
||||
|
|
|
@ -501,11 +501,12 @@ export async function createRandomSegments(fileDuration) {
|
|||
return edl;
|
||||
}
|
||||
|
||||
export async function showCutFailedDialog({ detectedFileFormat }) {
|
||||
export async function showExportFailedDialog({ detectedFileFormat, safeOutputFileName }) {
|
||||
const html = (
|
||||
<div style={{ textAlign: 'left' }}>
|
||||
<Trans>Try one of the following before exporting again:</Trans>
|
||||
<ol>
|
||||
{!safeOutputFileName && <li><Trans>Output file names are not sanitized. Try to enable sanitazion or check your segment labels for invalid characters.</Trans></li>}
|
||||
{detectedFileFormat === 'mp4' && <li><Trans>Change output <b>Format</b> from <b>MP4</b> to <b>MOV</b></Trans></li>}
|
||||
<li><Trans>Select a different output <b>Format</b> (<b>matroska</b> and <b>mp4</b> support most codecs)</Trans></li>
|
||||
<li><Trans>Disable unnecessary <b>Tracks</b></Trans></li>
|
||||
|
|
|
@ -16,10 +16,17 @@ export function getOutSegError({ fileNames, filePath, outputDir }) {
|
|||
break;
|
||||
}
|
||||
|
||||
const invalidChars = [pathSep];
|
||||
const invalidChars = new Set();
|
||||
|
||||
// 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(':');
|
||||
// https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
|
||||
if (isWindows) {
|
||||
['/', '<', '>', ':', '"', '\\', '|', '?', '*'].forEach((char) => invalidChars.add(char));
|
||||
} else if (isMac) {
|
||||
// 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
|
||||
['/', ':'].forEach((char) => invalidChars.add(char));
|
||||
} else {
|
||||
invalidChars.add(pathSep);
|
||||
}
|
||||
|
||||
const outPath = pathNormalize(pathJoin(outputDir, fileName));
|
||||
const sameAsInputPath = outPath === pathNormalize(filePath);
|
||||
|
@ -30,7 +37,7 @@ export function getOutSegError({ fileNames, filePath, outputDir }) {
|
|||
error = i18n.t('At least one resulting file name has no length');
|
||||
break;
|
||||
}
|
||||
if (invalidChars.some((c) => fileName.includes(c))) {
|
||||
if ([...fileName].some((char) => invalidChars.has(char))) {
|
||||
error = i18n.t('At least one resulting file name contains invalid characters');
|
||||
break;
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue