Add XMEML importer #458

pull/471/head
Mikael Finstad 2020-11-18 21:24:20 +01:00
rodzic 704a20ce73
commit 7e59ee6757
5 zmienionych plików z 53 dodań i 16 usunięć

Wyświetl plik

@ -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",

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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) => {
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'));
}
cutSegmentsHistory.go(0);
setCutSegments(edl.map(createSegment));
}, [cutSegmentsHistory, setCutSegments]);
const loadEdlFile = useCallback(async (edlPath, type = 'csv') => {
try {
const storedEdl = await edlStoreLoad(edlPath);
const allRowsValid = storedEdl
.every(row => row.start === undefined || row.end === undefined || row.start < row.end);
let storedEdl;
if (type === 'csv') storedEdl = await edlStoreLoad(edlPath);
else if (type === 'xmeml') storedEdl = await loadXmeml(edlPath);
if (!allRowsValid) {
throw new Error(i18n.t('Invalid start or end values for one or more segments'));
}
cutSegmentsHistory.go(0);
setCutSegments(storedEdl.map(createSegment));
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() {

Wyświetl plik

@ -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]);

Wyświetl plik

@ -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"