mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-25 19:52:44 +01:00
113 lines
3.4 KiB
JavaScript
113 lines
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);
|
|
}
|