kopia lustrzana https://github.com/mifi/lossless-cut
rodzic
f3ab17100d
commit
d5e03784d7
|
@ -44,6 +44,12 @@ module.exports = (app, mainWindow, newVersion) => {
|
|||
{
|
||||
label: 'Import project',
|
||||
submenu: [
|
||||
{
|
||||
label: 'EDL (MPlayer)',
|
||||
click() {
|
||||
mainWindow.webContents.send('importEdlFile', 'mplayer');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Text chapters / YouTube',
|
||||
click() {
|
||||
|
|
43
src/App.jsx
43
src/App.jsx
|
@ -47,7 +47,7 @@ import {
|
|||
findNearestKeyFrameTime as ffmpegFindNearestKeyFrameTime, isStreamThumbnail, isAudioSupported, isIphoneHevc, tryReadChaptersToEdl,
|
||||
getDuration, getTimecodeFromStreams, createChaptersFromSegments,
|
||||
} from './ffmpeg';
|
||||
import { saveCsv, saveTsv, loadCsv, loadXmeml, loadCue, loadPbf, saveCsvHuman } from './edlStore';
|
||||
import { saveCsv, saveTsv, loadCsv, loadXmeml, loadCue, loadPbf, loadMplayerEdl, saveCsvHuman } from './edlStore';
|
||||
import { formatYouTube } from './edlFormats';
|
||||
import {
|
||||
getOutPath, toast, errorToast, showFfmpegFail, setFileNameTitle, getOutDir, withBlur,
|
||||
|
@ -59,7 +59,7 @@ import { formatDuration } from './util/duration';
|
|||
import { askForOutDir, askForImportChapters, createNumSegments, createFixedDurationSegments, promptTimeOffset, askForHtml5ifySpeed, askForYouTubeInput, askForFileOpenAction, confirmExtractAllStreamsDialog, cleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog } from './dialogs';
|
||||
import { openSendReportDialog } from './reporting';
|
||||
import { fallbackLng } from './i18n';
|
||||
import { createSegment, createInitialCutSegments, getCleanCutSegments, getSegApparentStart, findSegmentsAtCursor } from './segments';
|
||||
import { createSegment, createInitialCutSegments, getCleanCutSegments, getSegApparentStart, findSegmentsAtCursor, sortSegments, invertSegments } from './segments';
|
||||
|
||||
|
||||
import loadingLottie from './7077-magic-flow.json';
|
||||
|
@ -315,46 +315,13 @@ const App = memo(() => {
|
|||
const jumpCutStart = useCallback(() => seekAbs(currentApparentCutSeg.start), [currentApparentCutSeg.start, seekAbs]);
|
||||
const jumpCutEnd = useCallback(() => seekAbs(currentApparentCutSeg.end), [currentApparentCutSeg.end, seekAbs]);
|
||||
|
||||
const sortedCutSegments = useMemo(() => sortBy(apparentCutSegments, 'start'), [apparentCutSegments]);
|
||||
const sortedCutSegments = useMemo(() => sortSegments(apparentCutSegments), [apparentCutSegments]);
|
||||
|
||||
const inverseCutSegments = useMemo(() => {
|
||||
if (haveInvalidSegs) return undefined;
|
||||
if (sortedCutSegments.length < 1) return undefined;
|
||||
|
||||
const foundOverlap = sortedCutSegments.some((cutSegment, i) => {
|
||||
if (i === 0) return false;
|
||||
return sortedCutSegments[i - 1].end > cutSegment.start;
|
||||
});
|
||||
|
||||
if (foundOverlap) return undefined;
|
||||
if (!isDurationValid(duration)) return undefined;
|
||||
|
||||
const ret = [];
|
||||
|
||||
if (sortedCutSegments[0].start > 0) {
|
||||
ret.push({
|
||||
start: 0,
|
||||
end: sortedCutSegments[0].start,
|
||||
});
|
||||
}
|
||||
|
||||
sortedCutSegments.forEach((cutSegment, i) => {
|
||||
if (i === 0) return;
|
||||
ret.push({
|
||||
start: sortedCutSegments[i - 1].end,
|
||||
end: cutSegment.start,
|
||||
});
|
||||
});
|
||||
|
||||
const last = sortedCutSegments[sortedCutSegments.length - 1];
|
||||
if (last.end < duration) {
|
||||
ret.push({
|
||||
start: last.end,
|
||||
end: duration,
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
return invertSegments(sortedCutSegments, duration);
|
||||
}, [duration, haveInvalidSegs, sortedCutSegments]);
|
||||
|
||||
const updateSegAtIndex = useCallback((index, newProps) => {
|
||||
|
@ -1191,6 +1158,7 @@ const App = memo(() => {
|
|||
else if (type === 'xmeml') edl = await loadXmeml(path);
|
||||
else if (type === 'cue') edl = await loadCue(path);
|
||||
else if (type === 'pbf') edl = await loadPbf(path);
|
||||
else if (type === 'mplayer') edl = await loadMplayerEdl(path);
|
||||
|
||||
loadCutSegments(edl);
|
||||
} catch (err) {
|
||||
|
@ -1713,6 +1681,7 @@ const App = memo(() => {
|
|||
else if (type === 'xmeml') filters = [{ name: i18n.t('XML files'), extensions: ['xml'] }];
|
||||
else if (type === 'cue') filters = [{ name: i18n.t('CUE files'), extensions: ['cue'] }];
|
||||
else if (type === 'pbf') filters = [{ name: i18n.t('PBF files'), extensions: ['pbf'] }];
|
||||
else if (type === 'mplayer') filters = [{ name: i18n.t('MPlayer EDL'), extensions: ['*'] }];
|
||||
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openFile'], filters });
|
||||
if (canceled || filePaths.length < 1) return;
|
||||
|
|
|
@ -15,7 +15,8 @@ import ToggleExportConfirm from './components/ToggleExportConfirm';
|
|||
import OutSegTemplateEditor from './components/OutSegTemplateEditor';
|
||||
import HighlightedText from './components/HighlightedText';
|
||||
|
||||
import { withBlur, toast, getSegColors } from './util';
|
||||
import { withBlur, toast } from './util';
|
||||
import { getSegColors } from './util/colors';
|
||||
import { isMov as ffmpegIsMov } from './ffmpeg';
|
||||
|
||||
const sheetStyle = {
|
||||
|
|
|
@ -7,7 +7,7 @@ import Swal from 'sweetalert2';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { saveColor } from './colors';
|
||||
import { getSegColors } from './util';
|
||||
import { getSegColors } from './util/colors';
|
||||
|
||||
const buttonBaseStyle = {
|
||||
margin: '0 3px', borderRadius: 3, color: 'white', cursor: 'pointer',
|
||||
|
|
|
@ -10,7 +10,7 @@ import InverseCutSegment from './InverseCutSegment';
|
|||
|
||||
import { timelineBackground } from './colors';
|
||||
|
||||
import { getSegColors } from './util';
|
||||
import { getSegColors } from './util/colors';
|
||||
|
||||
|
||||
const hammerOptions = { recognizers: {} };
|
||||
|
|
|
@ -5,7 +5,7 @@ import { IoMdKey } from 'react-icons/io';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
// import useTraceUpdate from 'use-trace-update';
|
||||
|
||||
import { getSegColors } from './util';
|
||||
import { getSegColors } from './util/colors';
|
||||
import { formatDuration, parseDuration } from './util/duration';
|
||||
import { primaryTextColor } from './colors';
|
||||
import SetCutpointButton from './components/SetCutpointButton';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { getSegColors } from '../util';
|
||||
import { getSegColors } from '../util/colors';
|
||||
|
||||
const SetCutpointButton = ({ currentCutSeg, side, Icon, onClick, title, style }) => {
|
||||
const {
|
||||
|
|
|
@ -6,6 +6,7 @@ import pify from 'pify';
|
|||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
import { formatDuration } from './util/duration';
|
||||
import { invertSegments, sortSegments } from './segments';
|
||||
|
||||
const csvParseAsync = pify(csvParse);
|
||||
|
||||
|
@ -32,6 +33,23 @@ export async function parseCsv(str) {
|
|||
return mapped;
|
||||
}
|
||||
|
||||
export async function parseMplayerEdl(text) {
|
||||
const cutAwaySegments = text.split('\n').map((line) => {
|
||||
// We only support "Cut" (0)
|
||||
const match = line.match(/^\s*([^\s]+)\s+([^\s]+)\s+0\s*$/);
|
||||
if (!match) return undefined;
|
||||
const start = parseFloat(match[1]);
|
||||
const end = parseFloat(match[2]);
|
||||
if (start < 0 || end < 0 || start >= end) throw new Error(i18n.t('Invalid start or end value. Must contain a number of seconds'));
|
||||
return { start, end };
|
||||
}).filter((it) => it);
|
||||
|
||||
if (cutAwaySegments.length === 0) throw new Error(i18n.t('Invalid EDL data found'));
|
||||
const inverted = invertSegments(sortSegments(cutAwaySegments));
|
||||
if (!inverted) throw new Error(i18n.t('Invalid EDL data found'));
|
||||
return inverted;
|
||||
}
|
||||
|
||||
export function parseCuesheet(cuesheet) {
|
||||
// There are 75 such frames per second of audio.
|
||||
// https://en.wikipedia.org/wiki/Cue_sheet_(computing)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { parseYouTube, formatYouTube } from './edlFormats';
|
||||
import { parseYouTube, formatYouTube, parseMplayerEdl } from './edlFormats';
|
||||
|
||||
it('parseYoutube', () => {
|
||||
const str = `
|
||||
|
@ -58,3 +58,41 @@ it('formatYouTube', () => {
|
|||
'0:00',
|
||||
]);
|
||||
});
|
||||
|
||||
// https://kodi.wiki/view/Edit_decision_list
|
||||
// http://www.mplayerhq.hu/DOCS/HTML/en/edl.html
|
||||
it('parseMplayerEdl', async () => {
|
||||
// TODO support more durations:
|
||||
/*
|
||||
const str = `\
|
||||
5.3 7.1 0
|
||||
15 16.7 1
|
||||
7:00 13:42 3
|
||||
1 4:15.3 2
|
||||
12:00.1 2
|
||||
`;
|
||||
const str = `\
|
||||
#127 #170 0
|
||||
#360 #400 1
|
||||
#10080 #19728 3
|
||||
#1 #6127 2
|
||||
#17282 2
|
||||
`;
|
||||
*/
|
||||
|
||||
const str = `\
|
||||
5.3 7.1 0
|
||||
15 16.7 1
|
||||
420 822 3
|
||||
1 255.3 2
|
||||
720.1 2
|
||||
`;
|
||||
|
||||
expect(await parseMplayerEdl(str)).toEqual([{ start: 0, end: 5.3 }, { start: 7.1, end: undefined }]);
|
||||
|
||||
const str2 = `\
|
||||
0 1.1 0
|
||||
`;
|
||||
|
||||
expect(await parseMplayerEdl(str2)).toEqual([{ start: 1.1, end: undefined }]);
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import csvStringify from 'csv-stringify';
|
||||
import pify from 'pify';
|
||||
|
||||
import { parseCuesheet, parseXmeml, parseCsv, parsePbf } from './edlFormats';
|
||||
import { parseCuesheet, parseXmeml, parseCsv, parsePbf, parseMplayerEdl } from './edlFormats';
|
||||
import { formatDuration } from './util/duration';
|
||||
|
||||
const fs = window.require('fs-extra');
|
||||
|
@ -21,6 +21,10 @@ export async function loadPbf(path) {
|
|||
return parsePbf(await fs.readFile(path, 'utf-8'));
|
||||
}
|
||||
|
||||
export async function loadMplayerEdl(path) {
|
||||
return parseMplayerEdl(await fs.readFile(path, 'utf-8'));
|
||||
}
|
||||
|
||||
export async function loadCue(path) {
|
||||
return parseCuesheet(cueParser.parse(path));
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import uuid from 'uuid';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
import { generateColor } from './util';
|
||||
import { generateColor } from './util/colors';
|
||||
|
||||
export const createSegment = ({ start, end, name } = {}) => ({
|
||||
start,
|
||||
|
@ -32,3 +33,43 @@ export function findSegmentsAtCursor(apparentSegments, currentTime) {
|
|||
});
|
||||
return indexes;
|
||||
}
|
||||
|
||||
export const sortSegments = (segments) => sortBy(segments, 'start');
|
||||
|
||||
export function invertSegments(sortedCutSegments, duration) {
|
||||
if (sortedCutSegments.length < 1) return undefined;
|
||||
|
||||
const foundOverlap = sortedCutSegments.some((cutSegment, i) => {
|
||||
if (i === 0) return false;
|
||||
return sortedCutSegments[i - 1].end > cutSegment.start;
|
||||
});
|
||||
|
||||
if (foundOverlap) return undefined;
|
||||
|
||||
const ret = [];
|
||||
|
||||
if (sortedCutSegments[0].start > 0) {
|
||||
ret.push({
|
||||
start: 0,
|
||||
end: sortedCutSegments[0].start,
|
||||
});
|
||||
}
|
||||
|
||||
sortedCutSegments.forEach((cutSegment, i) => {
|
||||
if (i === 0) return;
|
||||
ret.push({
|
||||
start: sortedCutSegments[i - 1].end,
|
||||
end: cutSegment.start,
|
||||
});
|
||||
});
|
||||
|
||||
const last = sortedCutSegments[sortedCutSegments.length - 1];
|
||||
if (last.end < duration || duration == null) {
|
||||
ret.push({
|
||||
start: last.end,
|
||||
end: duration,
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
16
src/util.js
16
src/util.js
|
@ -2,8 +2,6 @@ import Swal from 'sweetalert2';
|
|||
import i18n from 'i18next';
|
||||
import lodashTemplate from 'lodash/template';
|
||||
|
||||
import randomColor from './random-color';
|
||||
|
||||
const path = window.require('path');
|
||||
const fs = window.require('fs-extra');
|
||||
const open = window.require('open');
|
||||
|
@ -92,10 +90,6 @@ export function filenamify(name) {
|
|||
return name.replace(/[^0-9a-zA-Z_.]/g, '_');
|
||||
}
|
||||
|
||||
export function generateColor() {
|
||||
return randomColor(1, 0.95);
|
||||
}
|
||||
|
||||
export function withBlur(cb) {
|
||||
return (e) => {
|
||||
cb(e);
|
||||
|
@ -103,16 +97,6 @@ export function withBlur(cb) {
|
|||
};
|
||||
}
|
||||
|
||||
export function getSegColors(seg) {
|
||||
if (!seg) return {};
|
||||
const { color } = seg;
|
||||
return {
|
||||
segBgColor: color.alpha(0.5).string(),
|
||||
segActiveBgColor: color.lighten(0.5).alpha(0.5).string(),
|
||||
segBorderColor: color.lighten(0.5).string(),
|
||||
};
|
||||
}
|
||||
|
||||
export function dragPreventer(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import randomColor from './random-color';
|
||||
|
||||
export function generateColor() {
|
||||
return randomColor(1, 0.95);
|
||||
}
|
||||
|
||||
export function getSegColors(seg) {
|
||||
if (!seg) return {};
|
||||
const { color } = seg;
|
||||
return {
|
||||
segBgColor: color.alpha(0.5).string(),
|
||||
segActiveBgColor: color.lighten(0.5).alpha(0.5).string(),
|
||||
segBorderColor: color.lighten(0.5).string(),
|
||||
};
|
||||
}
|
Ładowanie…
Reference in New Issue