mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-22 10:22:31 +01:00
improvements
- improve concat/merge success dialog and error dialog - improve error classification (ENOENT)
This commit is contained in:
parent
f7047160a1
commit
97f7e2c88a
98
src/App.jsx
98
src/App.jsx
@ -65,14 +65,14 @@ import {
|
||||
checkDirWriteAccess, dirExists, isMasBuild, isStoreBuild, dragPreventer,
|
||||
filenamify, getOutFileExtension, generateSegFileName, defaultOutSegTemplate,
|
||||
havePermissionToReadFile, resolvePathIfNeeded, getPathReadAccessError, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile,
|
||||
deleteFiles, isOutOfSpaceError, shuffleArray, getNumDigits,
|
||||
deleteFiles, isOutOfSpaceError, shuffleArray, getNumDigits, isExecaFailure,
|
||||
} from './util';
|
||||
import { formatDuration } from './util/duration';
|
||||
import { adjustRate } from './util/rate-calculator';
|
||||
import { showParametersDialog } from './dialogs/parameters';
|
||||
import { askExtractFramesAsImages } from './dialogs/extractFrames';
|
||||
import { askForHtml5ifySpeed } from './dialogs/html5ify';
|
||||
import { askForOutDir, askForInputDir, askForImportChapters, createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, promptTimeOffset, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, openAbout, showEditableJsonDialog, askForShiftSegments, selectSegmentsByLabelDialog, showRefuseToOverwrite, openDirToast, openCutFinishedToast } from './dialogs';
|
||||
import { askForOutDir, askForInputDir, askForImportChapters, createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, promptTimeOffset, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, showConcatFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, openAbout, showEditableJsonDialog, askForShiftSegments, selectSegmentsByLabelDialog, showRefuseToOverwrite, openDirToast, openCutFinishedToast, openConcatFinishedToast } 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';
|
||||
@ -1105,6 +1105,48 @@ const App = memo(() => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const commonSettings = useMemo(() => ({
|
||||
ffmpegExperimental,
|
||||
preserveMovData,
|
||||
movFastStart,
|
||||
preserveMetadataOnMerge,
|
||||
}), [ffmpegExperimental, movFastStart, preserveMetadataOnMerge, preserveMovData]);
|
||||
|
||||
const openSendReportDialogWithState = useCallback(async (err) => {
|
||||
const state = {
|
||||
...commonSettings,
|
||||
|
||||
filePath,
|
||||
fileFormat,
|
||||
externalFilesMeta,
|
||||
mainStreams,
|
||||
copyStreamIdsByFile,
|
||||
cutSegments: cutSegments.map(s => ({ start: s.start, end: s.end })),
|
||||
mainFileFormatData,
|
||||
rotation,
|
||||
shortestFlag,
|
||||
effectiveExportMode,
|
||||
outSegTemplate,
|
||||
};
|
||||
|
||||
openSendReportDialog(err, state);
|
||||
}, [commonSettings, copyStreamIdsByFile, cutSegments, effectiveExportMode, externalFilesMeta, fileFormat, filePath, mainFileFormatData, mainStreams, outSegTemplate, rotation, shortestFlag]);
|
||||
|
||||
const openSendConcatReportDialogWithState = useCallback(async (err, reportState) => {
|
||||
const state = { ...commonSettings, ...reportState };
|
||||
openSendReportDialog(err, state);
|
||||
}, [commonSettings]);
|
||||
|
||||
const handleExportFailed = useCallback(async (err) => {
|
||||
const sendErrorReport = await showExportFailedDialog({ fileFormat, safeOutputFileName });
|
||||
if (sendErrorReport) openSendReportDialogWithState(err);
|
||||
}, [fileFormat, safeOutputFileName, openSendReportDialogWithState]);
|
||||
|
||||
const handleConcatFailed = useCallback(async (err, reportState) => {
|
||||
const sendErrorReport = await showConcatFailedDialog({ fileFormat });
|
||||
if (sendErrorReport) openSendConcatReportDialogWithState(err, reportState);
|
||||
}, [fileFormat, openSendConcatReportDialogWithState]);
|
||||
|
||||
const userConcatFiles = useCallback(async ({ paths, includeAllStreams, streams, fileFormat: outFormat, fileName: outFileName, clearBatchFilesAfterConcat }) => {
|
||||
if (workingRef.current) return;
|
||||
try {
|
||||
@ -1131,19 +1173,34 @@ const App = memo(() => {
|
||||
const metadataFromPath = paths[0];
|
||||
await concatFiles({ paths, outPath, outDir, outFormat, metadataFromPath, includeAllStreams, streams, ffmpegExperimental, onProgress: setCutProgress, preserveMovData, movFastStart, preserveMetadataOnMerge, chapters: chaptersFromSegments, appendFfmpegCommandLog });
|
||||
if (clearBatchFilesAfterConcat) closeBatch();
|
||||
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: outPath, text: i18n.t('Files merged!') });
|
||||
const notices = [];
|
||||
if (!includeAllStreams) notices.push(i18n.t('If your source files have more than two tracks, the extra tracks might have been removed. You can change this option before merging.'));
|
||||
if (!hideAllNotifications) openConcatFinishedToast({ filePath: outPath, notices });
|
||||
} catch (err) {
|
||||
if (isOutOfSpaceError(err)) {
|
||||
showDiskFull();
|
||||
if (err.killed === true) {
|
||||
// assume execa killed (aborted by user)
|
||||
return;
|
||||
}
|
||||
errorToast(i18n.t('Failed to merge files. Make sure they are all of the exact same codecs'));
|
||||
console.error('Failed to merge files', err);
|
||||
|
||||
console.error('stdout:', err.stdout);
|
||||
console.error('stderr:', err.stderr);
|
||||
|
||||
if (isExecaFailure(err)) {
|
||||
if (isOutOfSpaceError(err)) {
|
||||
showDiskFull();
|
||||
return;
|
||||
}
|
||||
const reportState = { includeAllStreams, streams, outFormat, outFileName, segmentsToChapters };
|
||||
handleConcatFailed(err, reportState);
|
||||
return;
|
||||
}
|
||||
|
||||
handleError(err);
|
||||
} finally {
|
||||
setWorking();
|
||||
setCutProgress();
|
||||
}
|
||||
}, [setWorking, ensureAccessibleDirectories, segmentsToChapters, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch, hideAllNotifications]);
|
||||
}, [setWorking, ensureAccessibleDirectories, segmentsToChapters, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch, hideAllNotifications, handleConcatFailed]);
|
||||
|
||||
const cleanupFiles = useCallback(async (cleanupChoices2) => {
|
||||
// Store paths before we reset state
|
||||
@ -1260,29 +1317,6 @@ const App = memo(() => {
|
||||
|
||||
const getOutSegError = useCallback((fileNames) => getOutSegErrorRaw({ fileNames, filePath, outputDir }), [outputDir, filePath]);
|
||||
|
||||
const openSendReportDialogWithState = useCallback(async (err) => {
|
||||
const state = {
|
||||
filePath,
|
||||
fileFormat,
|
||||
externalFilesMeta,
|
||||
mainStreams,
|
||||
copyStreamIdsByFile,
|
||||
cutSegments: cutSegments.map(s => ({ start: s.start, end: s.end })),
|
||||
mainFileFormatData,
|
||||
rotation,
|
||||
shortestFlag,
|
||||
effectiveExportMode,
|
||||
outSegTemplate,
|
||||
};
|
||||
|
||||
openSendReportDialog(err, state);
|
||||
}, [filePath, fileFormat, externalFilesMeta, mainStreams, copyStreamIdsByFile, cutSegments, mainFileFormatData, rotation, shortestFlag, effectiveExportMode, outSegTemplate]);
|
||||
|
||||
const handleExportFailed = useCallback(async (err) => {
|
||||
const sendErrorReport = await showExportFailedDialog({ detectedFileFormat, safeOutputFileName });
|
||||
if (sendErrorReport) openSendReportDialogWithState(err);
|
||||
}, [detectedFileFormat, safeOutputFileName, openSendReportDialogWithState]);
|
||||
|
||||
const closeExportConfirm = useCallback(() => setExportConfirmVisible(false), []);
|
||||
|
||||
const willMerge = segmentsToExport.length > 1 && autoMerge;
|
||||
@ -1409,7 +1443,7 @@ const App = memo(() => {
|
||||
console.error('stdout:', err.stdout);
|
||||
console.error('stderr:', err.stderr);
|
||||
|
||||
if (err.exitCode === 1 || err.code === 'ENOENT') {
|
||||
if (isExecaFailure(err)) {
|
||||
if (isOutOfSpaceError(err)) {
|
||||
showDiskFull();
|
||||
return;
|
||||
|
@ -210,7 +210,7 @@ const ConcatDialog = memo(({
|
||||
)}
|
||||
</Dialog>
|
||||
|
||||
<Dialog isShown={settingsVisible} onCloseComplete={() => setSettingsVisible(false)} title={t('Options')} hasCancel={false} confirmLabel={t('Close')}>
|
||||
<Dialog isShown={settingsVisible} onCloseComplete={() => setSettingsVisible(false)} title={t('Merge options')} hasCancel={false} confirmLabel={t('Close')}>
|
||||
<Checkbox checked={includeAllStreams} onChange={(e) => setIncludeAllStreams(e.target.checked)} label={`${t('Include all tracks?')} ${t('If this is checked, all audio/video/subtitle/data tracks will be included. This may not always work for all file types. If not checked, only default streams will be included.')}`} />
|
||||
|
||||
<Checkbox checked={preserveMetadataOnMerge} onChange={(e) => setPreserveMetadataOnMerge(e.target.checked)} label={t('Preserve original metadata when merging? (slow)')} />
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { HelpIcon, TickCircleIcon, UnorderedList, ListItem, WarningSignIcon, InfoSignIcon, Checkbox } from 'evergreen-ui';
|
||||
import { HelpIcon, TickCircleIcon, WarningSignIcon, InfoSignIcon, Checkbox } from 'evergreen-ui';
|
||||
import Swal from 'sweetalert2';
|
||||
import i18n from 'i18next';
|
||||
import { Trans } from 'react-i18next';
|
||||
@ -369,20 +369,27 @@ export async function createRandomSegments(fileDuration) {
|
||||
return edl;
|
||||
}
|
||||
|
||||
export async function showExportFailedDialog({ detectedFileFormat, safeOutputFileName }) {
|
||||
const MovSuggestion = ({ fileFormat }) => fileFormat === 'mp4' && <li><Trans>Change output <b>Format</b> from <b>MP4</b> to <b>MOV</b></Trans></li>;
|
||||
const OutputFormatSuggestion = () => <li><Trans>Select a different output <b>Format</b> (<b>matroska</b> and <b>mp4</b> support most codecs)</Trans></li>;
|
||||
const WorkingDirectorySuggestion = () => <li><Trans>Set a different <b>Working directory</b></Trans></li>;
|
||||
const DifferentFileSuggestion = () => <li><Trans>Try with a <b>Different file</b></Trans></li>;
|
||||
const HelpSuggestion = () => <li><Trans>See <b>Help</b></Trans> menu</li>;
|
||||
const ErrorReportSuggestion = () => <li><Trans>If nothing helps, you can send an <b>Error report</b></Trans></li>;
|
||||
|
||||
export async function showExportFailedDialog({ fileFormat, 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>
|
||||
<MovSuggestion fileFormat={fileFormat} />
|
||||
<OutputFormatSuggestion />
|
||||
<li><Trans>Disable unnecessary <b>Tracks</b></Trans></li>
|
||||
<li><Trans>Try both <b>Normal cut</b> and <b>Keyframe cut</b></Trans></li>
|
||||
<li><Trans>Set a different <b>Working directory</b></Trans></li>
|
||||
<li><Trans>Try with a <b>Different file</b></Trans></li>
|
||||
<li><Trans>See <b>Help</b></Trans> menu</li>
|
||||
<li><Trans>If nothing helps, you can send an <b>Error report</b></Trans></li>
|
||||
<WorkingDirectorySuggestion />
|
||||
<DifferentFileSuggestion />
|
||||
<HelpSuggestion />
|
||||
<ErrorReportSuggestion />
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
@ -391,6 +398,26 @@ export async function showExportFailedDialog({ detectedFileFormat, safeOutputFil
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function showConcatFailedDialog({ fileFormat }) {
|
||||
const html = (
|
||||
<div style={{ textAlign: 'left' }}>
|
||||
<Trans>Try each of the following before merging again:</Trans>
|
||||
<ol>
|
||||
<MovSuggestion fileFormat={fileFormat} />
|
||||
<OutputFormatSuggestion />
|
||||
<li><Trans>Disable <b>merge options</b></Trans></li>
|
||||
<WorkingDirectorySuggestion />
|
||||
<DifferentFileSuggestion />
|
||||
<HelpSuggestion />
|
||||
<ErrorReportSuggestion />
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
|
||||
const { value } = await ReactSwal.fire({ title: i18n.t('Unable to merge files'), html, timer: null, showConfirmButton: true, showCancelButton: true, cancelButtonText: i18n.t('OK'), confirmButtonText: i18n.t('Report'), reverseButtons: true, focusCancel: true });
|
||||
return value;
|
||||
}
|
||||
|
||||
export function openYouTubeChaptersDialog(text) {
|
||||
ReactSwal.fire({
|
||||
showCloseButton: true,
|
||||
@ -481,16 +508,36 @@ export async function openDirToast({ filePath, text, html, ...props }) {
|
||||
if (value) shell.showItemInFolder(filePath);
|
||||
}
|
||||
|
||||
const UnorderedList = ({ children }) => <ul style={{ paddingLeft: '1em' }}>{children}</ul>;
|
||||
const ListItem = ({ icon: Icon, iconColor, children }) => <li style={{ listStyle: 'none' }}>{Icon && <Icon color={iconColor} size={14} marginRight=".3em" />} {children}</li>;
|
||||
|
||||
const Notices = ({ notices }) => notices.map((msg) => <ListItem key={msg} icon={InfoSignIcon} iconColor="info">{msg}</ListItem>);
|
||||
const Warnings = ({ warnings }) => warnings.map((msg) => <ListItem key={msg} icon={WarningSignIcon} iconColor="warning">{msg}</ListItem>);
|
||||
const OutputIncorrectSeeHelpMenu = () => <ListItem icon={HelpIcon}>{i18n.t('If output does not look right, see the Help menu.')}</ListItem>;
|
||||
|
||||
export async function openCutFinishedToast({ filePath, warnings, notices }) {
|
||||
const html = (
|
||||
<UnorderedList>
|
||||
<ListItem icon={TickCircleIcon} iconColor="success" fontWeight="bold">{i18n.t('Export is done!')}</ListItem>
|
||||
<ListItem icon={InfoSignIcon}>{i18n.t('Note: cutpoints may be inaccurate. Please test the output files in your desired player/editor before you delete the source file.')}</ListItem>
|
||||
<ListItem icon={HelpIcon}>{i18n.t('If output does not look right, see the Help menu.')}</ListItem>
|
||||
{notices.map((msg) => <ListItem key={msg} icon={InfoSignIcon} iconColor="info">{msg}</ListItem>)}
|
||||
{warnings.map((msg) => <ListItem key={msg} icon={WarningSignIcon} iconColor="warning">{msg}</ListItem>)}
|
||||
<ListItem icon={InfoSignIcon} iconColor="info">{i18n.t('Note: cutpoints may be inaccurate. Please test the output files in your desired player/editor before you delete the source file.')}</ListItem>
|
||||
<OutputIncorrectSeeHelpMenu />
|
||||
<Notices notices={notices} />
|
||||
<Warnings warnings={warnings} />
|
||||
</UnorderedList>
|
||||
);
|
||||
|
||||
await openDirToast({ filePath, html, width: 800, position: 'center', timer: 15000 });
|
||||
await openDirToast({ filePath, html, width: 800, position: 'center', timer: 30000 });
|
||||
}
|
||||
|
||||
export async function openConcatFinishedToast({ filePath, notices }) {
|
||||
const html = (
|
||||
<UnorderedList>
|
||||
<ListItem icon={TickCircleIcon} iconColor="success" fontWeight="bold">{i18n.t('Files merged!')}</ListItem>
|
||||
<ListItem icon={InfoSignIcon} color="info">{i18n.t('Please test the output files in your desired player/editor before you delete the source files.')}</ListItem>
|
||||
<OutputIncorrectSeeHelpMenu />
|
||||
<Notices notices={notices} />
|
||||
</UnorderedList>
|
||||
);
|
||||
|
||||
await openDirToast({ filePath, html, width: 800, position: 'center', timer: 30000 });
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import i18n from 'i18next';
|
||||
import Timecode from 'smpte-timecode';
|
||||
|
||||
import { pcmAudioCodecs, getMapStreamsArgs, isMov } from './util/streams';
|
||||
import { getSuffixedOutPath, getExtensionForFormat, isWindows, isMac, platform, arch } from './util';
|
||||
import { getSuffixedOutPath, getExtensionForFormat, isWindows, isMac, platform, arch, isExecaFailure } from './util';
|
||||
import { isDurationValid } from './segments';
|
||||
|
||||
import isDev from './isDev';
|
||||
@ -364,7 +364,7 @@ export async function readFileMeta(filePath) {
|
||||
return { format, streams, chapters };
|
||||
} catch (err) {
|
||||
// Windows will throw error with code ENOENT if format detection fails.
|
||||
if (err.exitCode === 1 || (isWindows && err.code === 'ENOENT')) {
|
||||
if (isExecaFailure(err)) {
|
||||
const err2 = new Error(`Unsupported file: ${err.message}`);
|
||||
err2.code = 'LLC_FFPROBE_UNSUPPORTED_FILE';
|
||||
throw err2;
|
||||
|
@ -53,6 +53,8 @@ export function openSendReportDialog(err, state) {
|
||||
|
||||
<p><Trans>Include the following text:</Trans> <CopyClipboardButton text={text} /></p>
|
||||
|
||||
{!isStoreBuild && <p style={{ fontSize: '.8em', color: 'rgba(0,0,0,0.5)' }}><Trans>You might want to redact any sensitive information like paths.</Trans></p>}
|
||||
|
||||
<div style={{ fontWeight: 600, fontSize: 12, whiteSpace: 'pre-wrap', color: '#900' }} contentEditable suppressContentEditableWarning>
|
||||
{text}
|
||||
</div>
|
||||
|
@ -301,9 +301,13 @@ export const deleteDispositionValue = 'llc_disposition_remove';
|
||||
|
||||
export const mirrorTransform = 'matrix(-1, 0, 0, 1, 0, 0)';
|
||||
|
||||
// I *think* Windows will throw error with code ENOENT if ffprobe/ffmpeg fails (execa), but other OS'es will return this error code if a file is not found, so it would be wrong to attribute it to exec failure.
|
||||
// see https://github.com/mifi/lossless-cut/issues/451
|
||||
export const isExecaFailure = (err) => err.exitCode === 1 || (isWindows && err.code === 'ENOENT');
|
||||
|
||||
// A bit hacky but it works, unless someone has a file called "No space left on device" ( ͡° ͜ʖ ͡°)
|
||||
export const isOutOfSpaceError = (err) => (
|
||||
err && (err.exitCode === 1 || err.code === 'ENOENT')
|
||||
err && isExecaFailure(err)
|
||||
&& typeof err.stderr === 'string' && err.stderr.includes('No space left on device')
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user