diff --git a/src/App.jsx b/src/App.jsx index 3105174f..010eac73 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -68,7 +68,7 @@ import { getOutPath, getSuffixedOutPath, handleError, getOutDir, isStoreBuild, dragPreventer, havePermissionToReadFile, resolvePathIfNeeded, getPathReadAccessError, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile, - deleteFiles, isOutOfSpaceError, isExecaFailure, readFileSize, readFileSizes, checkFileSizes, setDocumentTitle, getOutFileExtension, getSuffixedFileName, mustDisallowVob, readVideoTs, getImportProjectType, + deleteFiles, isOutOfSpaceError, isExecaFailure, readFileSize, readFileSizes, checkFileSizes, setDocumentTitle, getOutFileExtension, getSuffixedFileName, mustDisallowVob, readVideoTs, readDirRecursively, getImportProjectType, calcShouldShowWaveform, calcShouldShowKeyframes, } from './util'; import { toast, errorToast } from './swal'; @@ -88,6 +88,7 @@ import isDev from './isDev'; const electron = window.require('electron'); const { exists } = window.require('fs-extra'); +const { lstat } = window.require('fs/promises'); const filePathToUrl = window.require('file-url'); const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path'); @@ -1798,12 +1799,28 @@ function App() { [lastOpenedPathRef.current] = filePaths; - // https://en.wikibooks.org/wiki/Inside_DVD-Video/Directory_Structure - if (filePaths.length === 1 && /^VIDEO_TS$/i.test(basename(filePaths[0]))) { - if (mustDisallowVob()) return; - filePaths = await readVideoTs(filePaths[0]); + // first check if it is a single directory, and if so, read it recursively + if (filePaths.length === 1) { + const firstFilePath = filePaths[0]; + const firstFileStat = await lstat(firstFilePath); + if (firstFileStat.isDirectory()) { + console.log('Reading directory...'); + filePaths = await readDirRecursively(firstFilePath); + } } + // Only allow opening regular files + // eslint-disable-next-line no-restricted-syntax + for (const path of filePaths) { + // eslint-disable-next-line no-await-in-loop + const fileStat = await lstat(path); + + if (!fileStat.isFile()) { + errorToast(i18n.t('Cannot open anything else than regular files')); + console.warn('Not a file:', path); + return; + } + } if (filePaths.length > 1) { if (alwaysConcatMultipleFiles) { @@ -1818,6 +1835,12 @@ function App() { // filePaths.length is now 1 const firstFilePath = filePaths[0]; + // https://en.wikibooks.org/wiki/Inside_DVD-Video/Directory_Structure + if (/^VIDEO_TS$/i.test(basename(firstFilePath))) { + if (mustDisallowVob()) return; + filePaths = await readVideoTs(firstFilePath); + } + if (workingRef.current) return; try { setWorking({ text: i18n.t('Loading file') }); @@ -1898,7 +1921,7 @@ function App() { }, [alwaysConcatMultipleFiles, batchLoadPaths, setWorking, isFileOpened, batchFiles.length, userOpenSingleFile, checkFileOpened, loadEdlFile, enableAskForFileOpenAction, addStreamSourceFile, filePath]); const openFilesDialog = useCallback(async () => { - const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile', 'multiSelections'], defaultPath: lastOpenedPathRef.current }); + const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile', 'openDirectory', 'multiSelections'], defaultPath: lastOpenedPathRef.current }); if (canceled) return; userOpenFiles(filePaths); }, [userOpenFiles]); diff --git a/src/util.js b/src/util.js index d45f04fd..7bfdbd8a 100644 --- a/src/util.js +++ b/src/util.js @@ -11,7 +11,7 @@ import { ffmpegExtractWindow } from './util/constants'; const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename } = window.require('path'); const fsExtra = window.require('fs-extra'); -const { stat, readdir, utimes, unlink } = window.require('fs/promises'); +const { stat, lstat, readdir, utimes, unlink } = window.require('fs/promises'); const os = window.require('os'); const { ipcRenderer } = window.require('electron'); const remote = window.require('@electron/remote'); @@ -90,7 +90,7 @@ export async function getPathReadAccessError(pathIn) { } export async function dirExists(dirPath) { - return (await pathExists(dirPath)) && (await fsExtra.lstat(dirPath)).isDirectory(); + return (await pathExists(dirPath)) && (await lstat(dirPath)).isDirectory(); } // const testFailFsOperation = isDev; @@ -402,6 +402,22 @@ export async function readVideoTs(videoTsPath) { return ret; } +export async function readDirRecursively(dirPath) { + const files = await readdir(dirPath, { recursive: true }); + const ret = (await pMap(files, async (path) => { + if (['.DS_Store'].includes(basename(path))) return []; + + const absPath = join(dirPath, path); + const fileStat = await lstat(absPath); // readdir also returns directories... + if (!fileStat.isFile()) return []; + + return [absPath]; + }, { concurrency: 5 })).flat(); + + if (ret.length === 0) throw new Error('No files found in folder'); + return ret; +} + export function getImportProjectType(filePath) { if (filePath.endsWith('Summary.txt')) return 'dv-analyzer-summary-txt'; const edlFormatForExtension = { csv: 'csv', pbf: 'pbf', edl: 'mplayer', cue: 'cue', xml: 'xmeml', fcpxml: 'fcpxml' };