lossless-cut/src/edlFormats.js

113 wiersze
3.4 KiB
JavaScript

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 };
});
}
export function parsePbf(text) {
const chapters = text.split('\n').map((line) => {
const match = line.match(/^[0-9]+=([0-9]+)\*([^*]+)*/);
if (match) return { time: parseInt(match[1], 10) / 1000, name: match[2] };
return undefined;
}).filter((it) => it);
const out = [];
chapters.forEach((chapter, i) => {
const nextChapter = chapters[i + 1];
out.push({ start: chapter.time, end: nextChapter && nextChapter.time, name: chapter.name });
});
return out;
}
// 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);
}