diff --git a/package.json b/package.json index fadd3bc7..980b3651 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "eslint-plugin-react": "^7.14.3", "eslint-plugin-react-hooks": "^1.7.0", "evergreen-ui": "^4.23.0", + "fast-xml-parser": "^3.17.4", "file-url": "^3.0.0", "framer-motion": "^1.8.4", "hammerjs": "^2.0.8", @@ -62,6 +63,7 @@ "mousetrap": "^1.6.1", "p-map": "^3.0.0", "patch-package": "^6.2.1", + "pify": "^5.0.0", "pretty-ms": "^6.0.0", "react": "^16.12.0", "react-dom": "^16.12.0", @@ -81,11 +83,11 @@ "wait-on": "^4.0.1" }, "dependencies": { + "cue-parser": "^0.3.0", "electron-is-dev": "^0.1.2", "electron-store": "^5.1.1", "electron-unhandled": "^3.0.2", "execa": "^4.0.0", - "fast-xml-parser": "^3.17.4", "ffmpeg-static": "^4.2.1", "ffprobe-static": "^3.0.0", "file-type": "^12.4.0", diff --git a/public/configStore.js b/public/configStore.js index f0ed4654..623a7164 100644 --- a/public/configStore.js +++ b/public/configStore.js @@ -10,6 +10,8 @@ const defaults = { invertCutSegments: false, autoExportExtraStreams: true, askBeforeClose: false, + enableAskForImportChapters: true, + enableAskForFileOpenAction: true, muted: false, autoSaveProjectFile: true, wheelSensitivity: 0.2, diff --git a/public/menu.js b/public/menu.js index 1132808c..af98be41 100644 --- a/public/menu.js +++ b/public/menu.js @@ -38,12 +38,24 @@ module.exports = (app, mainWindow, newVersion) => { { label: 'Import project', submenu: [ + { + label: 'Text chapters / YouTube', + click() { + mainWindow.webContents.send('importEdlFile', 'youtube'); + }, + }, { label: 'DaVinci Resolve / Final Cut Pro XML', click() { mainWindow.webContents.send('importEdlFile', 'xmeml'); }, }, + { + label: 'CUE sheet file', + click() { + mainWindow.webContents.send('importEdlFile', 'cue'); + }, + }, ], }, { diff --git a/src/App.jsx b/src/App.jsx index aecb0bb8..b81c7cd5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -41,13 +41,14 @@ import { defaultProcessedCodecTypes, getStreamFps, isCuttingStart, isCuttingEnd, getDefaultOutFormat, getFormatData, mergeFiles as ffmpegMergeFiles, renderThumbnails as ffmpegRenderThumbnails, readFrames, renderWaveformPng, html5ifyDummy, cutMultiple, extractStreams, autoMergeSegments, getAllStreams, - findNearestKeyFrameTime, html5ify as ffmpegHtml5ify, isStreamThumbnail, isAudioSupported, isIphoneHevc, + findNearestKeyFrameTime, html5ify as ffmpegHtml5ify, isStreamThumbnail, isAudioSupported, isIphoneHevc, tryReadChaptersToEdl, } from './ffmpeg'; -import { save as edlStoreSave, load as edlStoreLoad, loadXmeml } from './edlStore'; +import { saveCsv, loadCsv, loadXmeml, loadCue } from './edlStore'; import { getOutPath, formatDuration, toast, errorToast, showFfmpegFail, setFileNameTitle, promptTimeOffset, generateColor, getOutDir, withBlur, checkDirWriteAccess, dirExists, askForOutDir, - openDirToast, askForHtml5ifySpeed, isMasBuild, isStoreBuild, + openDirToast, askForHtml5ifySpeed, askForYouTubeInput, isMasBuild, isStoreBuild, askForFileOpenAction, + askForImportChapters, } from './util'; import { openSendReportDialog } from './reporting'; import { fallbackLng } from './i18n'; @@ -213,6 +214,10 @@ const App = memo(() => { useEffect(() => safeSetConfig('autoExportExtraStreams', autoExportExtraStreams), [autoExportExtraStreams]); const [askBeforeClose, setAskBeforeClose] = useState(configStore.get('askBeforeClose')); useEffect(() => safeSetConfig('askBeforeClose', askBeforeClose), [askBeforeClose]); + const [enableAskForImportChapters, setEnableAskForImportChapters] = useState(configStore.get('enableAskForImportChapters')); + useEffect(() => safeSetConfig('enableAskForImportChapters', enableAskForImportChapters), [enableAskForImportChapters]); + const [enableAskForFileOpenAction, setEnableAskForFileOpenAction] = useState(configStore.get('enableAskForFileOpenAction')); + useEffect(() => safeSetConfig('enableAskForFileOpenAction', enableAskForFileOpenAction), [enableAskForFileOpenAction]); const [muted, setMuted] = useState(configStore.get('muted')); useEffect(() => safeSetConfig('muted', muted), [muted]); const [autoSaveProjectFile, setAutoSaveProjectFile] = useState(configStore.get('autoSaveProjectFile')); @@ -566,7 +571,7 @@ const App = memo(() => { return; } */ - await edlStoreSave(edlFilePath, debouncedCutSegments); + await saveCsv(edlFilePath, debouncedCutSegments); lastSavedCutSegmentsRef.current = debouncedCutSegments; } catch (err) { errorToast(i18n.t('Unable to save project file')); @@ -1106,29 +1111,26 @@ const App = memo(() => { }, []); const loadCutSegments = useCallback((edl) => { - const allRowsValid = edl - .every(row => row.start === undefined || row.end === undefined || row.start < row.end); + if (edl.length === 0) throw new Error(); + const allRowsValid = edl.every(row => row.start === undefined || row.end === undefined || row.start < row.end); - if (!allRowsValid) { - throw new Error(i18n.t('Invalid start or end values for one or more segments')); - } + if (!allRowsValid) throw new Error(i18n.t('Invalid start or end values for one or more segments')); cutSegmentsHistory.go(0); setCutSegments(edl.map(createSegment)); }, [cutSegmentsHistory, setCutSegments]); - const loadEdlFile = useCallback(async (edlPath, type = 'csv') => { + const loadEdlFile = useCallback(async (path, type = 'csv') => { try { - let storedEdl; - if (type === 'csv') storedEdl = await edlStoreLoad(edlPath); - else if (type === 'xmeml') storedEdl = await loadXmeml(edlPath); + let edl; + if (type === 'csv') edl = await loadCsv(path); + else if (type === 'xmeml') edl = await loadXmeml(path); + else if (type === 'cue') edl = await loadCue(path); - loadCutSegments(storedEdl); + loadCutSegments(edl); } catch (err) { - if (err.code !== 'ENOENT') { - console.error('EDL load failed', err); - errorToast(`${i18n.t('Failed to load project file')} (${err.message})`); - } + console.error('EDL load failed', err); + errorToast(`${i18n.t('Failed to load project')} (${err.message})`); } }, [loadCutSegments]); @@ -1216,7 +1218,17 @@ const App = memo(() => { await createDummyVideo(cod, fp); } - await loadEdlFile(getEdlFilePath(fp)); + const openedFileEdlPath = getEdlFilePath(fp); + + if (await exists(openedFileEdlPath)) { + await loadEdlFile(openedFileEdlPath); + } else { + const edl = await tryReadChaptersToEdl(fp); + if (edl.length > 0 && enableAskForImportChapters && (await askForImportChapters())) { + console.log('Read chapters', edl); + loadCutSegments(edl); + } + } } catch (err) { if (err.exitCode === 1 || err.code === 'ENOENT') { errorToast(i18n.t('Unsupported file')); @@ -1227,7 +1239,7 @@ const App = memo(() => { } finally { setWorking(); } - }, [resetState, working, createDummyVideo, loadEdlFile, getEdlFilePath, getHtml5ifiedPath]); + }, [resetState, working, createDummyVideo, loadEdlFile, getEdlFilePath, getHtml5ifiedPath, loadCutSegments, enableAskForImportChapters]); const toggleHelp = useCallback(() => setHelpVisible(val => !val), []); const toggleSettings = useCallback(() => setSettingsVisible(val => !val), []); @@ -1363,27 +1375,15 @@ const App = memo(() => { return; } - const { value } = await Swal.fire({ - title: i18n.t('You opened a new file. What do you want to do?'), - icon: 'question', - input: 'radio', - inputValue: 'open', - showCancelButton: true, - customClass: { input: 'swal2-losslesscut-radio' }, - inputOptions: { - open: i18n.t('Open the file instead of the current one'), - add: i18n.t('Include all tracks from the new file'), - }, - inputValidator: (v) => !v && i18n.t('You need to choose something!'), - }); + const openFileResponse = enableAskForFileOpenAction ? await askForFileOpenAction() : 'open'; - if (value === 'open') { + if (openFileResponse === 'open') { load({ filePath: firstFile, customOutDir: newCustomOutDir }); - } else if (value === 'add') { + } else if (openFileResponse === 'add') { addStreamSourceFile(firstFile); setStreamsSelectorShown(true); } - }, [addStreamSourceFile, isFileOpened, load, mergeFiles, assureOutDirAccess]); + }, [addStreamSourceFile, isFileOpened, load, mergeFiles, assureOutDirAccess, enableAskForFileOpenAction]); const onDrop = useCallback(async (ev) => { ev.preventDefault(); @@ -1525,7 +1525,7 @@ const App = memo(() => { errorToast(i18n.t('File exists, bailing')); return; } - await edlStoreSave(fp, cutSegments); + await saveCsv(fp, cutSegments); } catch (err) { errorToast(i18n.t('Failed to export CSV')); console.error('Failed to export CSV', err); @@ -1538,9 +1538,16 @@ const App = memo(() => { return; } + if (type === 'youtube') { + const edl = await askForYouTubeInput(); + if (edl.length > 0) loadCutSegments(edl); + return; + } + let filters; if (type === 'csv') filters = [{ name: i18n.t('CSV files'), extensions: ['csv'] }]; else if (type === 'xmeml') filters = [{ name: i18n.t('XML files'), extensions: ['xml'] }]; + else if (type === 'cue') filters = [{ name: i18n.t('CUE files'), extensions: ['cue'] }]; const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openFile'], filters }); if (canceled || filePaths.length < 1) return; @@ -1650,6 +1657,7 @@ const App = memo(() => { mergeFiles, outputDir, filePath, isFileOpened, customOutDir, startTimeOffset, html5ifyCurrentFile, createDummyVideo, resetState, extractAllStreams, userOpenFiles, cutSegmentsHistory, openSendReportDialogWithState, loadEdlFile, cutSegments, edlFilePath, askBeforeClose, toggleHelp, toggleSettings, assureOutDirAccess, html5ifyAndLoad, html5ifyInternal, + loadCutSegments, ]); async function showAddStreamSourceDialog() { @@ -1735,6 +1743,10 @@ const App = memo(() => { setTimecodeShowFrames={setTimecodeShowFrames} askBeforeClose={askBeforeClose} setAskBeforeClose={setAskBeforeClose} + enableAskForImportChapters={enableAskForImportChapters} + setEnableAskForImportChapters={setEnableAskForImportChapters} + enableAskForFileOpenAction={enableAskForFileOpenAction} + setEnableAskForFileOpenAction={setEnableAskForFileOpenAction} ffmpegExperimental={ffmpegExperimental} setFfmpegExperimental={setFfmpegExperimental} invertTimelineScroll={invertTimelineScroll} @@ -1747,7 +1759,7 @@ const App = memo(() => { renderCaptureFormatButton={renderCaptureFormatButton} onWheelTunerRequested={onWheelTunerRequested} /> - ), [AutoExportToggler, askBeforeClose, autoMerge, autoSaveProjectFile, customOutDir, invertCutSegments, keyframeCut, renderCaptureFormatButton, renderOutFmt, timecodeShowFrames, changeOutDir, onWheelTunerRequested, language, invertTimelineScroll, ffmpegExperimental, setFfmpegExperimental]); + ), [AutoExportToggler, askBeforeClose, autoMerge, autoSaveProjectFile, customOutDir, invertCutSegments, keyframeCut, renderCaptureFormatButton, renderOutFmt, timecodeShowFrames, changeOutDir, onWheelTunerRequested, language, invertTimelineScroll, ffmpegExperimental, setFfmpegExperimental, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction]); useEffect(() => { if (!isStoreBuild) loadMifiLink().then(setMifiLink); diff --git a/src/Settings.jsx b/src/Settings.jsx index f1d3a117..819af2c7 100644 --- a/src/Settings.jsx +++ b/src/Settings.jsx @@ -8,6 +8,7 @@ const Settings = memo(({ autoSaveProjectFile, setAutoSaveProjectFile, timecodeShowFrames, setTimecodeShowFrames, askBeforeClose, setAskBeforeClose, renderOutFmt, AutoExportToggler, renderCaptureFormatButton, onWheelTunerRequested, language, setLanguage, invertTimelineScroll, setInvertTimelineScroll, ffmpegExperimental, setFfmpegExperimental, + enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, }) => { const { t } = useTranslation(); @@ -187,6 +188,28 @@ const Settings = memo(({ /> + + + {t('Ask about importing chapters from opened file?')} + + setEnableAskForImportChapters(e.target.checked)} + /> + + + + + {t('Ask about what to do when opening a new file when another file is already already open?')} + + setEnableAskForFileOpenAction(e.target.checked)} + /> + + ); }); diff --git a/src/edlFormats.js b/src/edlFormats.js new file mode 100644 index 00000000..9ef396bc --- /dev/null +++ b/src/edlFormats.js @@ -0,0 +1,96 @@ +import fastXmlParser from 'fast-xml-parser'; +import i18n from 'i18next'; + +import csvParse from 'csv-parse'; +import pify from 'pify'; +import sortBy from 'lodash/sortBy'; + +const csvParseAsync = pify(csvParse); + +export async function parseCsv(str) { + const rows = await csvParseAsync(str, {}); + if (rows.length === 0) throw new Error(i18n.t('No rows found')); + if (!rows.every(row => row.length === 3)) throw new Error(i18n.t('One or more rows does not have 3 columns')); + + const mapped = rows + .map(([start, end, name]) => ({ + start: start === '' ? undefined : parseFloat(start, 10), + end: end === '' ? undefined : parseFloat(end, 10), + name, + })); + + if (!mapped.every(({ start, end }) => ( + (start === undefined || !Number.isNaN(start)) + && (end === undefined || !Number.isNaN(end)) + ))) { + console.log(mapped); + throw new Error(i18n.t('Invalid start or end value. Must contain a number of seconds')); + } + + return mapped; +} + +export function parseCuesheet(cuesheet) { + // There are 75 such frames per second of audio. + // https://en.wikipedia.org/wiki/Cue_sheet_(computing) + const fps = 75; + + const { tracks } = cuesheet.files[0]; + + function parseTime(track) { + const index = track.indexes[0]; + if (!index) return undefined; + const { time } = index; + if (!time) return undefined; + + return (time.min * 60) + time.sec + time.frame / fps; + } + + return tracks.map((track, i) => { + const nextTrack = tracks[i + 1]; + const end = nextTrack && parseTime(nextTrack); + + return { name: track.title, start: parseTime(track), end }; + }); +} + +// https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/FinalCutPro_XML/VersionsoftheInterchangeFormat/VersionsoftheInterchangeFormat.html +export function parseXmeml(xmlStr) { + const xml = fastXmlParser.parse(xmlStr); + // TODO maybe support media.audio also? + return xml.xmeml.project.children.sequence.media.video.track.clipitem.map((item) => ({ start: item.start / item.rate.timebase, end: item.end / item.rate.timebase })); +} + +export function parseYouTube(str) { + const regex = /(?:([0-9]{2,}):)?([0-9]{2}):([0-9]{2})(?:\.([0-9]{3}))?[^\S\n]+([^\n]*)\n/g; + + const lines = []; + + function parseLine(match) { + if (!match) return undefined; + const [, hourStr, minStr, secStr, msStr, name] = match; + const hour = hourStr != null ? parseInt(hourStr, 10) : 0; + const min = parseInt(minStr, 10); + const sec = parseInt(secStr, 10); + const ms = msStr != null ? parseInt(msStr, 10) : 0; + + const time = (((hour * 60) + min) * 60 + sec) + ms / 1000; + + return { time, name }; + } + + let m; + // eslint-disable-next-line no-cond-assign + while ((m = regex.exec(`${str}\n`))) { + lines.push(parseLine(m)); + } + + const linesSorted = sortBy(lines, (l) => l.time); + + const edl = linesSorted.map((line, i) => { + const nextLine = linesSorted[i + 1]; + return { start: line.time, end: nextLine && nextLine.time, name: line.name }; + }); + + return edl.filter((ed) => ed.start !== ed.end); +} diff --git a/src/edlFormats.test.js b/src/edlFormats.test.js new file mode 100644 index 00000000..1812e49e --- /dev/null +++ b/src/edlFormats.test.js @@ -0,0 +1,44 @@ +/* eslint-disable no-undef */ +import { parseYouTube } from './edlFormats'; + +it('parseYoutube', () => { + const str = ` +Jump to chapters: +00:00 Test 1 +00:01 Test 2 +00:02 00:57 double +00:01:01 Test 3 +01:01:01.012 Test 4 +00:01:01.012 Test 5 + 01:01.012 Test 6 + :01:02.012 Test 7 +00:57:01.0123 Invalid 2 +00:57:01. Invalid 3 +01:15: Invalid 4 +0132 Invalid 5 +00:03 +00:04 +00:05 +`; + const edl = parseYouTube(str); + expect(edl).toEqual([ + { start: 0, end: 1, name: 'Test 1' }, + { start: 1, end: 2, name: 'Test 2' }, + { start: 2, end: 4, name: '00:57 double' }, + { start: 4, end: 5, name: '' }, + { start: 5, end: 61, name: '' }, + { start: 61, end: 61.012, name: 'Test 3' }, + { start: 61.012, end: 62.012, name: 'Test 6' }, + { start: 62.012, end: 3661.012, name: 'Test 7' }, + { start: 3661.012, end: undefined, name: 'Test 4' }, + ]); +}); + +it('parseYouTube eol', () => { + const str = ' 00:00 Test 1\n00:01 Test 2'; + const edl = parseYouTube(str); + expect(edl).toEqual([ + { start: 0, end: 1, name: 'Test 1' }, + { start: 1, end: undefined, name: 'Test 2' }, + ]); +}); diff --git a/src/edlStore.js b/src/edlStore.js index c2f2369b..e8b21eb9 100644 --- a/src/edlStore.js +++ b/src/edlStore.js @@ -1,48 +1,28 @@ -import parse from 'csv-parse'; -import stringify from 'csv-stringify'; -import i18n from 'i18next'; -import fastXmlParser from 'fast-xml-parser'; +import csvStringify from 'csv-stringify'; +import pify from 'pify'; -const fs = window.require('fs-extra'); -const { promisify } = window.require('util'); - -const stringifyAsync = promisify(stringify); -const parseAsync = promisify(parse); - -export async function load(path) { - const str = await fs.readFile(path, 'utf-8'); - const rows = await parseAsync(str, {}); - if (rows.length === 0) throw new Error(i18n.t('No rows found')); - if (!rows.every(row => row.length === 3)) throw new Error(i18n.t('One or more rows does not have 3 columns')); +import { parseCuesheet, parseXmeml, parseCsv } from './edlFormats'; - const mapped = rows - .map(([start, end, name]) => ({ - start: start === '' ? undefined : parseFloat(start, 10), - end: end === '' ? undefined : parseFloat(end, 10), - name, - })); +const fs = window.require('fs-extra'); +const cueParser = window.require('cue-parser'); - if (!mapped.every(({ start, end }) => ( - (start === undefined || !Number.isNaN(start)) - && (end === undefined || !Number.isNaN(end)) - ))) { - console.log(mapped); - throw new Error(i18n.t('Invalid start or end value. Must contain a number of seconds')); - } +const csvStringifyAsync = pify(csvStringify); - return mapped; +export async function loadCsv(path) { + return parseCsv(await fs.readFile(path, 'utf-8')); } -// https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/FinalCutPro_XML/VersionsoftheInterchangeFormat/VersionsoftheInterchangeFormat.html export async function loadXmeml(path) { - const xml = fastXmlParser.parse(await fs.readFile(path, 'utf-8')); - // TODO maybe support media.audio also? - return xml.xmeml.project.children.sequence.media.video.track.clipitem.map((item) => ({ start: item.start / item.rate.timebase, end: item.end / item.rate.timebase })); + return parseXmeml(await fs.readFile(path, 'utf-8')); +} + +export async function loadCue(path) { + return parseCuesheet(cueParser.parse(path)); } -export async function save(path, cutSegments) { +export async function saveCsv(path, cutSegments) { console.log('Saving', path); const rows = cutSegments.map(({ start, end, name }) => [start, end, name]); - const str = await stringifyAsync(rows); + const str = await csvStringifyAsync(rows); await fs.writeFile(path, str); } diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 69054ee6..6c2dc5ad 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -344,6 +344,28 @@ export async function getDuration(filePath) { return parseFloat(JSON.parse(stdout).format.duration); } +export async function tryReadChaptersToEdl(filePath) { + try { + const { stdout } = await runFfprobe(['-i', filePath, '-show_chapters', '-print_format', 'json']); + return JSON.parse(stdout).chapters.map((chapter) => { + const start = parseInt(chapter.start_time, 10); + const end = parseInt(chapter.end_time, 10); + if (Number.isNaN(start) || Number.isNaN(end)) return undefined; + + const name = chapter.tags && typeof chapter.tags.title === 'string' ? chapter.tags.title : undefined; + + return { + start, + end, + name, + }; + }).filter((it) => it); + } catch (err) { + console.error('Failed to read chapters from file', err); + return []; + } +} + export async function html5ify({ filePath, outPath, video, audio, onProgress }) { console.log('Making HTML5 friendly version', { filePath, outPath, video, audio }); diff --git a/src/util.js b/src/util.js index 38f1b2bf..b5c95d5d 100644 --- a/src/util.js +++ b/src/util.js @@ -4,6 +4,8 @@ import i18n from 'i18next'; import randomColor from './random-color'; +import { parseYouTube } from './edlFormats'; + const path = window.require('path'); const fs = window.require('fs-extra'); const open = window.require('open'); @@ -197,6 +199,26 @@ export async function askForHtml5ifySpeed(allowedOptions) { return value; } +export async function askForYouTubeInput() { + const example = i18n.t('YouTube video description\n00:00 Intro\n00:01 Chapter 2\n00:00:02.123 Chapter 3'); + const { value } = await Swal.fire({ + title: i18n.t('Import text chapters / YouTube'), + input: 'textarea', + inputPlaceholder: example, + text: i18n.t('Paste or type a YouTube chapters description or textual chapter description'), + showCancelButton: true, + inputValidator: (v) => { + if (v) { + const edl = parseYouTube(v); + if (edl.length > 0) return undefined; + } + return i18n.t('Please input a valid format.'); + }, + }); + + return parseYouTube(value); +} + export const isMasBuild = window.process.mas; export const isStoreBuild = isMasBuild || window.process.windowsStore; @@ -210,3 +232,33 @@ export async function askForOutDir(defaultPath) { }); return (filePaths && filePaths.length === 1) ? filePaths[0] : undefined; } + +export async function askForFileOpenAction() { + const { value } = await Swal.fire({ + title: i18n.t('You opened a new file. What do you want to do?'), + icon: 'question', + input: 'radio', + inputValue: 'open', + showCancelButton: true, + customClass: { input: 'swal2-losslesscut-radio' }, + inputOptions: { + open: i18n.t('Open the file instead of the current one'), + add: i18n.t('Include all tracks from the new file'), + }, + inputValidator: (v) => !v && i18n.t('You need to choose something!'), + }); + + return value; +} + +export async function askForImportChapters() { + const { value } = await Swal.fire({ + icon: 'question', + text: i18n.t('This file has embedded chapters. Do you want to import the chapters as cut-segments?'), + showCancelButton: true, + cancelButtonText: i18n.t('Ignore chapters'), + confirmButtonText: i18n.t('Import chapters'), + }); + + return value; +} diff --git a/yarn.lock b/yarn.lock index 23eec6cb..98865e14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3203,6 +3203,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +chardet@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-1.3.0.tgz#a56ed2d9e4517a7128721340a0cb9a10a8fac238" + integrity sha512-cyTQGGptIjIT+CMGT5J/0l9c6Fb+565GCFjjeUTKxUO7w3oR+FcNCMEKTn5xtVKaLFmladN7QF68IiQsv5Fbdw== + chokidar@^2.0.2, chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -4110,6 +4115,13 @@ csv-stringify@^5.3.6: resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-5.3.6.tgz#2655e2e1c01b97b3963bccbc9407b8fb876dc589" integrity sha512-kPcRbMvo5NLLD71TAqW5K+g9kbM2HpIZJLAzm73Du8U+5TXmDp9YtXKCBLyxEh0q3Jbg8QhNFBz3b5VJzjZ/jw== +cue-parser@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/cue-parser/-/cue-parser-0.3.0.tgz#813f4d47329b14bba998c6abdb7ee3f49147722f" + integrity sha512-dT9aE78B10akUWxt5SYhsyrGbIpoO67ELOk4DzIpwfhQNjauS9gxZ2SoOnwAfVUr2HAxFRH/8gH/8Umn4DYRow== + dependencies: + chardet "^1.0.0" + cyclist@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" @@ -9748,6 +9760,11 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== +pify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" + integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== + pinkie-promise@^2.0.0, pinkie-promise@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"