From 7e59ee6757ae0628451d8d44fe427734610e57a4 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Wed, 18 Nov 2020 21:24:20 +0100 Subject: [PATCH] Add XMEML importer #458 --- package.json | 1 + public/menu.js | 13 ++++++++++++- src/App.jsx | 42 +++++++++++++++++++++++++++--------------- src/edlStore.js | 8 ++++++++ yarn.lock | 5 +++++ 5 files changed, 53 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 05c5ebc2..fadd3bc7 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "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/menu.js b/public/menu.js index 72697ab2..1132808c 100644 --- a/public/menu.js +++ b/public/menu.js @@ -32,9 +32,20 @@ module.exports = (app, mainWindow, newVersion) => { { label: 'Load project (CSV)', click() { - mainWindow.webContents.send('importEdlFile'); + mainWindow.webContents.send('importEdlFile', 'csv'); }, }, + { + label: 'Import project', + submenu: [ + { + label: 'DaVinci Resolve / Final Cut Pro XML', + click() { + mainWindow.webContents.send('importEdlFile', 'xmeml'); + }, + }, + ], + }, { label: 'Save project (CSV)', click() { diff --git a/src/App.jsx b/src/App.jsx index 9b865e2e..aecb0bb8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -43,7 +43,7 @@ import { readFrames, renderWaveformPng, html5ifyDummy, cutMultiple, extractStreams, autoMergeSegments, getAllStreams, findNearestKeyFrameTime, html5ify as ffmpegHtml5ify, isStreamThumbnail, isAudioSupported, isIphoneHevc, } from './ffmpeg'; -import { save as edlStoreSave, load as edlStoreLoad } from './edlStore'; +import { save as edlStoreSave, load as edlStoreLoad, loadXmeml } from './edlStore'; import { getOutPath, formatDuration, toast, errorToast, showFfmpegFail, setFileNameTitle, promptTimeOffset, generateColor, getOutDir, withBlur, checkDirWriteAccess, dirExists, askForOutDir, @@ -1105,25 +1105,32 @@ const App = memo(() => { return getOutPath(cod, fp, `html5ified-${type}.${ext}`); }, []); - const loadEdlFile = useCallback(async (edlPath) => { - try { - const storedEdl = await edlStoreLoad(edlPath); - const allRowsValid = storedEdl - .every(row => row.start === undefined || row.end === undefined || row.start < row.end); + const loadCutSegments = useCallback((edl) => { + 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]); - cutSegmentsHistory.go(0); - setCutSegments(storedEdl.map(createSegment)); + const loadEdlFile = useCallback(async (edlPath, type = 'csv') => { + try { + let storedEdl; + if (type === 'csv') storedEdl = await edlStoreLoad(edlPath); + else if (type === 'xmeml') storedEdl = await loadXmeml(edlPath); + + loadCutSegments(storedEdl); } catch (err) { if (err.code !== 'ENOENT') { console.error('EDL load failed', err); errorToast(`${i18n.t('Failed to load project file')} (${err.message})`); } } - }, [cutSegmentsHistory, setCutSegments]); + }, [loadCutSegments]); const load = useCallback(async ({ filePath: fp, customOutDir: cod, html5FriendlyPathRequested, dummyVideoPathRequested }) => { console.log('Load', { fp, cod, html5FriendlyPathRequested, dummyVideoPathRequested }); @@ -1525,14 +1532,19 @@ const App = memo(() => { } } - async function importEdlFile() { + async function importEdlFile(e, type) { if (!isFileOpened) { toast.fire({ icon: 'info', title: i18n.t('You need to open a media file first') }); return; } - const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openFile'], filters: [{ name: i18n.t('CSV files'), extensions: ['csv'] }] }); + + 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'] }]; + + const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openFile'], filters }); if (canceled || filePaths.length < 1) return; - await loadEdlFile(filePaths[0]); + await loadEdlFile(filePaths[0], type); } function openHelp() { diff --git a/src/edlStore.js b/src/edlStore.js index 5b5431bb..c2f2369b 100644 --- a/src/edlStore.js +++ b/src/edlStore.js @@ -1,6 +1,7 @@ import parse from 'csv-parse'; import stringify from 'csv-stringify'; import i18n from 'i18next'; +import fastXmlParser from 'fast-xml-parser'; const fs = window.require('fs-extra'); const { promisify } = window.require('util'); @@ -32,6 +33,13 @@ export async function load(path) { return mapped; } +// 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 })); +} + export async function save(path, cutSegments) { console.log('Saving', path); const rows = cutSegments.map(({ start, end, name }) => [start, end, name]); diff --git a/yarn.lock b/yarn.lock index 0a932b25..23eec6cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5461,6 +5461,11 @@ fast-shallow-equal@^1.0.0: resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b" integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw== +fast-xml-parser@^3.17.4: + version "3.17.4" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.17.4.tgz#d668495fb3e4bbcf7970f3c24ac0019d82e76477" + integrity sha512-qudnQuyYBgnvzf5Lj/yxMcf4L9NcVWihXJg7CiU1L+oUCq8MUnFEfH2/nXR/W5uq+yvUN1h7z6s7vs2v1WkL1A== + fastest-stable-stringify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028"