diff --git a/package.json b/package.json
index 19e75076..93965d4e 100644
--- a/package.json
+++ b/package.json
@@ -71,6 +71,7 @@
"mkdirp": "^1.0.3",
"mousetrap": "^1.6.5",
"p-map": "^5.5.0",
+ "p-retry": "^6.1.0",
"pify": "^5.0.0",
"pretty-bytes": "^6.0.0",
"react": "^18.2.0",
diff --git a/src/App.jsx b/src/App.jsx
index 0561893b..cbc1d606 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -171,13 +171,19 @@ const App = memo(() => {
const [selectedBatchFiles, setSelectedBatchFiles] = useState([]);
// Store "working" in a ref so we can avoid race conditions
- const workingRef = useRef(working);
+ const workingRef = useRef(!!working);
const setWorking = useCallback((val) => {
- workingRef.current = val;
- setWorkingState(val);
+ workingRef.current = !!val;
+ setWorkingState(val ? { text: val.text, abortController: val.abortController } : undefined);
}, []);
- useEffect(() => setDocumentTitle({ filePath, working, cutProgress }), [cutProgress, filePath, working]);
+ const handleAbortWorkingClick = useCallback(() => {
+ console.log('User clicked abort');
+ abortFfmpegs(); // todo use abortcontroller for this also
+ working?.abortController?.abort();
+ }, [working?.abortController]);
+
+ useEffect(() => setDocumentTitle({ filePath, working: working?.text, cutProgress }), [cutProgress, filePath, working?.text]);
const zoom = Math.floor(zoomUnrounded);
@@ -593,7 +599,7 @@ const App = memo(() => {
const subtitleStream = index != null && subtitleStreams.find((s) => s.index === index);
if (!subtitleStream || workingRef.current) return;
try {
- setWorking(i18n.t('Loading subtitle'));
+ setWorking({ text: i18n.t('Loading subtitle') });
const url = await extractSubtitleTrack(filePath, index);
setSubtitlesByStreamId((old) => ({ ...old, [index]: { url, lang: subtitleStream.tags && subtitleStream.tags.language } }));
setActiveSubtitleStreamIndex(index);
@@ -805,7 +811,7 @@ const App = memo(() => {
if (workingRef.current) return;
try {
- setWorking(i18n.t('Batch converting to supported format'));
+ setWorking({ text: i18n.t('Batch converting to supported format') });
setCutProgress(0);
// eslint-disable-next-line no-restricted-syntax
@@ -841,7 +847,7 @@ const App = memo(() => {
const html5ifyAndLoadWithPreferences = useCallback(async (cod, fp, speed, hv, ha) => {
if (!enableAutoHtml5ify) return;
- setWorking(i18n.t('Converting to supported format'));
+ setWorking({ text: i18n.t('Converting to supported format') });
await html5ifyAndLoad(cod, fp, getConvertToSupportedFormat(speed), hv, ha);
}, [enableAutoHtml5ify, setWorking, html5ifyAndLoad, getConvertToSupportedFormat]);
@@ -996,7 +1002,7 @@ const App = memo(() => {
if (workingRef.current) return;
try {
setConcatDialogVisible(false);
- setWorking(i18n.t('Merging'));
+ setWorking({ text: i18n.t('Merging') });
const firstPath = paths[0];
if (!firstPath) return;
@@ -1069,7 +1075,8 @@ const App = memo(() => {
}
try {
- setWorking(i18n.t('Cleaning up'));
+ const abortController = new AbortController();
+ setWorking({ text: i18n.t('Cleaning up'), abortController });
console.log('Cleaning up files', cleanupChoices2);
const pathsToDelete = [];
@@ -1077,7 +1084,7 @@ const App = memo(() => {
if (cleanupChoices2.trashProjectFile && savedPaths.projectFilePath) pathsToDelete.push(savedPaths.projectFilePath);
if (cleanupChoices2.trashSourceFile && savedPaths.sourceFilePath) pathsToDelete.push(savedPaths.sourceFilePath);
- await deleteFiles(pathsToDelete, cleanupChoices2.deleteIfTrashFails);
+ await deleteFiles({ paths: pathsToDelete, deleteIfTrashFails: cleanupChoices2.deleteIfTrashFails, signal: abortController.signal });
} catch (err) {
errorToast(i18n.t('Unable to delete file: {{message}}', { message: err.message }));
console.error(err);
@@ -1151,7 +1158,7 @@ const App = memo(() => {
if (workingRef.current) return;
try {
- setWorking(i18n.t('Exporting'));
+ setWorking({ text: i18n.t('Exporting') });
// Special segments-to-chapters mode:
let chaptersToAdd;
@@ -1199,7 +1206,7 @@ const App = memo(() => {
if (willMerge) {
setCutProgress(0);
- setWorking(i18n.t('Merging'));
+ setWorking({ text: i18n.t('Merging') });
const chapterNames = segmentsToChapters && !invertCutSegments ? segmentsToExport.map((s) => s.name) : undefined;
@@ -1235,7 +1242,7 @@ const App = memo(() => {
if (exportExtraStreams) {
try {
setCutProgress(); // If extracting extra streams takes a long time, prevent loader from being stuck at 100%
- setWorking(i18n.t('Extracting {{count}} unprocessable tracks', { count: nonCopiedExtraStreams.length }));
+ setWorking({ text: i18n.t('Extracting {{count}} unprocessable tracks', { count: nonCopiedExtraStreams.length }) });
await extractStreams({ filePath, customOutDir, streams: nonCopiedExtraStreams, enableOverwriteOutput });
notices.push(i18n.t('Unprocessable streams were exported as separate files.'));
} catch (err) {
@@ -1311,7 +1318,7 @@ const App = memo(() => {
if (captureFramesResponse == null) return;
try {
- setWorking(i18n.t('Extracting frames'));
+ setWorking({ text: i18n.t('Extracting frames') });
console.log('Extracting frames as images', { segIds, captureFramesResponse });
setCutProgress(0);
@@ -1411,7 +1418,7 @@ const App = memo(() => {
}
}
- setWorking(i18n.t('Loading file'));
+ setWorking({ text: i18n.t('Loading file') });
try {
// Need to check if file is actually readable
const pathReadAccessErrorCode = await getPathReadAccessError(fp);
@@ -1586,7 +1593,7 @@ const App = memo(() => {
if (workingRef.current) return;
if (filePath === path) return;
try {
- setWorking(i18n.t('Loading file'));
+ setWorking({ text: i18n.t('Loading file') });
await userOpenSingleFile({ path });
} catch (err) {
handleError(err);
@@ -1645,7 +1652,7 @@ const App = memo(() => {
if (workingRef.current) return;
try {
- setWorking(i18n.t('Extracting all streams'));
+ setWorking({ text: i18n.t('Extracting all streams') });
setStreamsSelectorShown(false);
const extractedPaths = await extractStreams({ customOutDir, filePath, streams: mainCopiedStreams, enableOverwriteOutput });
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: extractedPaths[0], text: i18n.t('All streams have been extracted as separate files') });
@@ -1685,7 +1692,7 @@ const App = memo(() => {
if (workingRef.current) return;
try {
- setWorking(i18n.t('Converting to supported format'));
+ setWorking({ text: i18n.t('Converting to supported format') });
await html5ifyAndLoad(customOutDir, filePath, selectedOption, hasVideo, hasAudio);
} catch (err) {
errorToast(i18n.t('Failed to convert file. Try a different conversion'));
@@ -1712,7 +1719,7 @@ const App = memo(() => {
const tryFixInvalidDuration = useCallback(async () => {
if (!checkFileOpened() || workingRef.current) return;
try {
- setWorking(i18n.t('Fixing file duration'));
+ setWorking({ text: i18n.t('Fixing file duration') });
setCutProgress(0);
const path = await fixInvalidDuration({ fileFormat, customOutDir, duration, onProgress: setCutProgress });
if (!hideAllNotifications) toast.fire({ icon: 'info', text: i18n.t('Duration has been fixed') });
@@ -1809,7 +1816,7 @@ const App = memo(() => {
if (workingRef.current) return;
try {
- setWorking(i18n.t('Loading file'));
+ setWorking({ text: i18n.t('Loading file') });
// Import segments for for already opened file
const matchingImportProjectType = getImportProjectType(firstFilePath);
@@ -2075,7 +2082,7 @@ const App = memo(() => {
if (workingRef.current) return;
try {
- setWorking(i18n.t('Extracting track'));
+ setWorking({ text: i18n.t('Extracting track') });
// setStreamsSelectorShown(false);
const extractedPaths = await extractStreams({ customOutDir, filePath, streams: mainStreams.filter((s) => s.index === index), enableOverwriteOutput });
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: extractedPaths[0], text: i18n.t('Track has been extracted') });
@@ -2111,7 +2118,7 @@ const App = memo(() => {
if (workingRef.current) return;
try {
- setWorking(i18n.t('Converting to supported format'));
+ setWorking({ text: i18n.t('Converting to supported format') });
console.log('Trying to create preview');
@@ -2388,7 +2395,7 @@ const App = memo(() => {
)}
- {working && abortFfmpegs()} />}
+ {working && }
{tunerVisible && setTunerVisible()} />}
diff --git a/src/hooks/useSegments.js b/src/hooks/useSegments.js
index 00f8bde8..0e12b938 100644
--- a/src/hooks/useSegments.js
+++ b/src/hooks/useSegments.js
@@ -84,7 +84,7 @@ export default ({
if (!filePath) return;
if (workingRef.current) return;
try {
- setWorking(workingText);
+ setWorking({ text: workingText });
setCutProgress(0);
const newSegments = await fn();
@@ -250,7 +250,7 @@ export default ({
try {
const response = await askForAlignSegments();
if (response == null) return;
- setWorking(i18n.t('Aligning segments to keyframes'));
+ setWorking({ text: i18n.t('Aligning segments to keyframes') });
const { mode, startOrEnd } = response;
await modifySelectedSegmentTimes(async (segment) => {
const newSegment = { ...segment };
diff --git a/src/util.js b/src/util.js
index 24be5d2f..5b4e3b02 100644
--- a/src/util.js
+++ b/src/util.js
@@ -3,6 +3,7 @@ import pMap from 'p-map';
import ky from 'ky';
import prettyBytes from 'pretty-bytes';
import sortBy from 'lodash/sortBy';
+import pRetry from 'p-retry';
import isDev from './isDev';
import Swal, { toast } from './swal';
@@ -234,14 +235,19 @@ export function getHtml5ifiedPath(cod, fp, type) {
return getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: `${html5ifiedPrefix}${type}.${ext}` });
}
-export async function deleteFiles(paths, deleteIfTrashFails) {
+export async function deleteFiles({ paths, deleteIfTrashFails, signal }) {
const failedToTrashFiles = [];
+ // const testFail = isDev;
+ const testFail = false;
+
// eslint-disable-next-line no-restricted-syntax
for (const path of paths) {
try {
+ if (testFail) throw new Error('test trash failure');
// eslint-disable-next-line no-await-in-loop
await trashFile(path);
+ signal.throwIfAborted();
} catch (err) {
console.error(err);
failedToTrashFiles.push(path);
@@ -260,7 +266,19 @@ export async function deleteFiles(paths, deleteIfTrashFails) {
if (!value) return;
}
- await pMap(failedToTrashFiles, async (path) => unlink(path), { concurrency: 1 });
+ // Retry because sometimes it fails on windows #272 #1797
+ await pMap(failedToTrashFiles, async (path) => {
+ await pRetry(async () => {
+ if (testFail) throw new Error('test delete failure');
+ await unlink(path);
+ }, {
+ retries: 3,
+ signal,
+ onFailedAttempt: async () => {
+ console.warn('Retrying delete', path);
+ },
+ });
+ }, { concurrency: 1 });
}
export const deleteDispositionValue = 'llc_disposition_remove';
diff --git a/yarn.lock b/yarn.lock
index 850908d6..db5d320f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1703,6 +1703,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/retry@npm:0.12.2":
+ version: 0.12.2
+ resolution: "@types/retry@npm:0.12.2"
+ checksum: e5675035717b39ce4f42f339657cae9637cf0c0051cf54314a6a2c44d38d91f6544be9ddc0280587789b6afd056be5d99dbe3e9f4df68c286c36321579b1bf4a
+ languageName: node
+ linkType: hard
+
"@types/scheduler@npm:*":
version: 0.16.2
resolution: "@types/scheduler@npm:0.16.2"
@@ -5964,6 +5971,13 @@ __metadata:
languageName: node
linkType: hard
+"is-network-error@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "is-network-error@npm:1.0.0"
+ checksum: 2ca2b4b2d420015e0237abe28ebf316fcd26a82304b07432abf155759a3bee6895609ac91e692a72ad61b7fc902c3283b2dece61e1ddb05a6257777a8573e468
+ languageName: node
+ linkType: hard
+
"is-number-object@npm:^1.0.4":
version: 1.0.6
resolution: "is-number-object@npm:1.0.6"
@@ -6597,6 +6611,7 @@ __metadata:
morgan: ^1.10.0
mousetrap: ^1.6.5
p-map: ^5.5.0
+ p-retry: ^6.1.0
pify: ^5.0.0
pretty-bytes: ^6.0.0
react: ^18.2.0
@@ -7543,6 +7558,17 @@ __metadata:
languageName: node
linkType: hard
+"p-retry@npm:^6.1.0":
+ version: 6.1.0
+ resolution: "p-retry@npm:6.1.0"
+ dependencies:
+ "@types/retry": 0.12.2
+ is-network-error: ^1.0.0
+ retry: ^0.13.1
+ checksum: 1083b2b72672205680f8a736583e31dce5d4ae472996cd06f4a33cd7ea11798d7712c202d253eb8afbdc80abf52f049651989c59f2e2ccca529e6b64d722b1f7
+ languageName: node
+ linkType: hard
+
"p-try@npm:^1.0.0":
version: 1.0.0
resolution: "p-try@npm:1.0.0"
@@ -8484,6 +8510,13 @@ __metadata:
languageName: node
linkType: hard
+"retry@npm:^0.13.1":
+ version: 0.13.1
+ resolution: "retry@npm:0.13.1"
+ checksum: 47c4d5be674f7c13eee4cfe927345023972197dbbdfba5d3af7e461d13b44de1bfd663bfc80d2f601f8ef3fc8164c16dd99655a221921954a65d044a2fc1233b
+ languageName: node
+ linkType: hard
+
"reusify@npm:^1.0.4":
version: 1.0.4
resolution: "reusify@npm:1.0.4"