kopia lustrzana https://github.com/mifi/lossless-cut
implement import csv with frame numbers #1024
rodzic
3b3444fc7e
commit
fe88b3d6f6
|
@ -52,11 +52,17 @@ module.exports = (app, mainWindow, newVersion) => {
|
|||
label: i18n.t('Import project'),
|
||||
submenu: [
|
||||
{
|
||||
label: i18n.t('LosslessCut (CSV)'),
|
||||
label: i18n.t('Times in seconds (CSV)'),
|
||||
click() {
|
||||
mainWindow.webContents.send('importEdlFile', 'csv');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.t('Frame numbers (CSV)'),
|
||||
click() {
|
||||
mainWindow.webContents.send('importEdlFile', 'csv-frames');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.t('EDL (MPlayer)'),
|
||||
click() {
|
||||
|
@ -93,7 +99,7 @@ module.exports = (app, mainWindow, newVersion) => {
|
|||
label: i18n.t('Export project'),
|
||||
submenu: [
|
||||
{
|
||||
label: i18n.t('LosslessCut (CSV)'),
|
||||
label: i18n.t('Times in seconds (CSV)'),
|
||||
click() {
|
||||
mainWindow.webContents.send('exportEdlFile', 'csv');
|
||||
},
|
||||
|
|
14
src/App.jsx
14
src/App.jsx
|
@ -53,7 +53,7 @@ import {
|
|||
getDuration, getTimecodeFromStreams, createChaptersFromSegments, extractSubtitleTrack,
|
||||
} from './ffmpeg';
|
||||
import { exportEdlFile, readEdlFile, saveLlcProject, loadLlcProject, askForEdlImport } from './edlStore';
|
||||
import { formatYouTube } from './edlFormats';
|
||||
import { formatYouTube, getTimeFromFrameNum as getTimeFromFrameNumRaw, getFrameCountRaw } from './edlFormats';
|
||||
import {
|
||||
getOutPath, toast, errorToast, handleError, setFileNameTitle, getOutDir, withBlur,
|
||||
checkDirWriteAccess, dirExists, openDirToast, isMasBuild, isStoreBuild, dragPreventer, doesPlayerSupportFile,
|
||||
|
@ -446,10 +446,9 @@ const App = memo(() => {
|
|||
setCutSegments(sortBy(cutSegments, getSegApparentStart));
|
||||
}, [cutSegments, setCutSegments]);
|
||||
|
||||
const getFrameCount = useCallback((sec) => {
|
||||
if (detectedFps == null) return undefined;
|
||||
return Math.floor(sec * detectedFps);
|
||||
}, [detectedFps]);
|
||||
const getFrameCount = useCallback((sec) => getFrameCountRaw(detectedFps, sec), [detectedFps]);
|
||||
|
||||
const getTimeFromFrameNum = useCallback((frameNum) => getTimeFromFrameNumRaw(detectedFps, frameNum), [detectedFps]);
|
||||
|
||||
const formatTimecode = useCallback(({ seconds, shorten }) => {
|
||||
if (timecodeFormat === 'frameCount') {
|
||||
|
@ -1129,7 +1128,6 @@ const App = memo(() => {
|
|||
// Emulate a single segment with no cuts (full timeline)
|
||||
segmentsToExport = [{ start: 0, end: getSegApparentEnd({}) }];
|
||||
chaptersToAdd = sortBy(enabledOutSegments, 'start').map((segment) => ({ start: segment.start, end: segment.end, name: segment.name }));
|
||||
console.log(chaptersToAdd);
|
||||
}
|
||||
|
||||
console.log('outSegTemplateOrDefault', outSegTemplateOrDefault);
|
||||
|
@ -1911,7 +1909,7 @@ const App = memo(() => {
|
|||
if (!checkFileOpened()) return;
|
||||
|
||||
try {
|
||||
const edl = await askForEdlImport(type);
|
||||
const edl = await askForEdlImport({ type, getTimeFromFrameNum });
|
||||
if (edl.length > 0) loadCutSegments(edl, true);
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
|
@ -2049,7 +2047,7 @@ const App = memo(() => {
|
|||
outputDir, filePath, customOutDir, startTimeOffset, userHtml5ifyCurrentFile, batchLoadPaths,
|
||||
extractAllStreams, userOpenFiles, openSendReportDialogWithState, setWorking,
|
||||
loadEdlFile, cutSegments, apparentCutSegments, edlFilePath, toggleHelp, toggleSettings, ensureOutDirAccessible, html5ifyAndLoad, html5ify,
|
||||
loadCutSegments, duration, checkFileOpened, loadMedia, fileFormat, reorderSegsByStartTime, closeFileWithConfirm, closeBatch, clearSegments, clearSegCounter, fixInvalidDuration, invertAllCutSegments, getFrameCount,
|
||||
loadCutSegments, duration, checkFileOpened, loadMedia, fileFormat, reorderSegsByStartTime, closeFileWithConfirm, closeBatch, clearSegments, clearSegCounter, fixInvalidDuration, invertAllCutSegments, getFrameCount, getTimeFromFrameNum,
|
||||
]);
|
||||
|
||||
const showAddStreamSourceDialog = useCallback(async () => {
|
||||
|
|
|
@ -2,6 +2,7 @@ import fastXmlParser from 'fast-xml-parser';
|
|||
import i18n from 'i18next';
|
||||
|
||||
import csvParse from 'csv-parse/lib/browser';
|
||||
import csvStringify from 'csv-stringify/lib/browser';
|
||||
import pify from 'pify';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
|
@ -10,16 +11,29 @@ import { formatDuration } from './util/duration';
|
|||
import { invertSegments, sortSegments } from './segments';
|
||||
|
||||
const csvParseAsync = pify(csvParse);
|
||||
const csvStringifyAsync = pify(csvStringify);
|
||||
|
||||
export async function parseCsv(str) {
|
||||
const rows = await csvParseAsync(str, {});
|
||||
export const getTimeFromFrameNum = (detectedFps, frameNum) => frameNum / detectedFps;
|
||||
|
||||
export function getFrameCountRaw(detectedFps, sec) {
|
||||
if (detectedFps == null) return undefined;
|
||||
return Math.floor(sec * detectedFps);
|
||||
}
|
||||
|
||||
export async function parseCsv(csvStr, processTime = (t) => t) {
|
||||
const rows = await csvParseAsync(csvStr, {});
|
||||
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'));
|
||||
|
||||
function parseTimeVal(str) {
|
||||
if (str === '') return undefined;
|
||||
const parsed = parseFloat(str, 10);
|
||||
return processTime(parsed);
|
||||
}
|
||||
const mapped = rows
|
||||
.map(([start, end, name]) => ({
|
||||
start: start === '' ? undefined : parseFloat(start, 10),
|
||||
end: end === '' ? undefined : parseFloat(end, 10),
|
||||
start: parseTimeVal(start),
|
||||
end: parseTimeVal(end),
|
||||
name,
|
||||
}));
|
||||
|
||||
|
@ -168,3 +182,35 @@ export function formatYouTube(segments) {
|
|||
return `${timeStr}${namePart}`;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
const safeFormatDuration = (duration) => (duration != null ? formatDuration({ seconds: duration }) : '');
|
||||
const safeFormatFrameCount = ({ seconds, getFrameCount }) => (seconds != null ? getFrameCount(seconds) : '');
|
||||
|
||||
export const formatSegmentsTimes = (cutSegments) => cutSegments.map(({ start, end, name }) => [
|
||||
safeFormatDuration(start),
|
||||
safeFormatDuration(end),
|
||||
name,
|
||||
]);
|
||||
|
||||
const formatSegmentsFrameCounts = ({ cutSegments, getFrameCount }) => cutSegments.map(({ start, end, name }) => [
|
||||
safeFormatFrameCount({ seconds: start, getFrameCount }),
|
||||
safeFormatFrameCount({ seconds: end, getFrameCount }),
|
||||
name,
|
||||
]);
|
||||
|
||||
export async function formatCsvFrames({ cutSegments, getFrameCount }) {
|
||||
return csvStringifyAsync(formatSegmentsFrameCounts({ cutSegments, getFrameCount }));
|
||||
}
|
||||
|
||||
export async function formatCsvSeconds(cutSegments) {
|
||||
const rows = cutSegments.map(({ start, end, name }) => [start, end, name]);
|
||||
return csvStringifyAsync(rows);
|
||||
}
|
||||
|
||||
export async function formatCsvHuman(cutSegments) {
|
||||
return csvStringifyAsync(formatSegmentsTimes(cutSegments));
|
||||
}
|
||||
|
||||
export async function formatTsv(cutSegments) {
|
||||
return csvStringifyAsync(formatSegmentsTimes(cutSegments), { delimiter: '\t' });
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import fs from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { parseYouTube, formatYouTube, parseMplayerEdl, parseXmeml } from './edlFormats';
|
||||
import { parseYouTube, formatYouTube, parseMplayerEdl, parseXmeml, parseCsv, getTimeFromFrameNum, formatCsvFrames, getFrameCountRaw } from './edlFormats';
|
||||
|
||||
const readFixture = async (name, encoding = 'utf-8') => fs.promises.readFile(join(__dirname, 'fixtures', name), encoding);
|
||||
|
||||
|
@ -139,3 +139,29 @@ it('parses xmeml 1', async () => {
|
|||
it('parses xmeml 2', async () => {
|
||||
expect(await parseXmeml(await readFixture('Final Cut Pro XMEML 2.xml'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// https://github.com/mifi/lossless-cut/issues/1024
|
||||
const csvFramesStr = `\
|
||||
0,155,EP106_SQ010_SH0010
|
||||
156,251,EP106_SQ010_SH0020
|
||||
252,687,EP106_SQ010_SH0030
|
||||
688,747,EP106_SQ020_SH0010
|
||||
`;
|
||||
|
||||
it('parses csv with frames', async () => {
|
||||
const fps = 30;
|
||||
const parsed = await parseCsv(csvFramesStr, (frameCount) => getTimeFromFrameNum(fps, frameCount));
|
||||
|
||||
expect(parsed).toEqual([
|
||||
{ end: 5.166666666666667, name: 'EP106_SQ010_SH0010', start: 0 },
|
||||
{ end: 8.366666666666667, name: 'EP106_SQ010_SH0020', start: 5.2 },
|
||||
{ end: 22.9, name: 'EP106_SQ010_SH0030', start: 8.4 },
|
||||
{ end: 24.9, name: 'EP106_SQ020_SH0010', start: 22.933333333333334 },
|
||||
]);
|
||||
|
||||
const formatted = await formatCsvFrames({
|
||||
cutSegments: parsed,
|
||||
getFrameCount: (sec) => getFrameCountRaw(fps, sec),
|
||||
});
|
||||
expect(formatted).toEqual(csvFramesStr);
|
||||
});
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import csvStringify from 'csv-stringify/lib/browser';
|
||||
import pify from 'pify';
|
||||
import JSON5 from 'json5';
|
||||
import i18n from 'i18next';
|
||||
|
||||
import { parseCuesheet, parseXmeml, parseCsv, parsePbf, parseMplayerEdl } from './edlFormats';
|
||||
import { formatDuration } from './util/duration';
|
||||
import { parseCuesheet, parseXmeml, parseCsv, parsePbf, parseMplayerEdl, formatCsvHuman, formatTsv, formatCsvFrames, formatCsvSeconds } from './edlFormats';
|
||||
import { askForYouTubeInput } from './dialogs';
|
||||
|
||||
const fs = window.require('fs-extra');
|
||||
|
@ -14,12 +11,14 @@ const { basename } = window.require('path');
|
|||
const electron = window.require('electron'); // eslint-disable-line
|
||||
const { dialog } = electron.remote;
|
||||
|
||||
const csvStringifyAsync = pify(csvStringify);
|
||||
|
||||
export async function loadCsv(path) {
|
||||
export async function loadCsvSeconds(path) {
|
||||
return parseCsv(await fs.readFile(path, 'utf-8'));
|
||||
}
|
||||
|
||||
export async function loadCsvFrames(path, getTimeFromFrameNum) {
|
||||
return parseCsv(await fs.readFile(path, 'utf-8'), (frameNum) => getTimeFromFrameNum(frameNum));
|
||||
}
|
||||
|
||||
export async function loadXmeml(path) {
|
||||
return parseXmeml(await fs.readFile(path, 'utf-8'));
|
||||
}
|
||||
|
@ -37,38 +36,19 @@ export async function loadCue(path) {
|
|||
}
|
||||
|
||||
export async function saveCsv(path, cutSegments) {
|
||||
const rows = cutSegments.map(({ start, end, name }) => [start, end, name]);
|
||||
const str = await csvStringifyAsync(rows);
|
||||
await fs.writeFile(path, str);
|
||||
await fs.writeFile(path, await formatCsvSeconds(cutSegments));
|
||||
}
|
||||
|
||||
const safeFormatDuration = (duration) => (duration != null ? formatDuration({ seconds: duration }) : '');
|
||||
const safeFormatFrameCount = ({ seconds, getFrameCount }) => (seconds != null ? getFrameCount(seconds) : '');
|
||||
|
||||
const formatSegmentsTimes = (cutSegments) => cutSegments.map(({ start, end, name }) => [
|
||||
safeFormatDuration(start),
|
||||
safeFormatDuration(end),
|
||||
name,
|
||||
]);
|
||||
const formatSegmentsFrameCounts = ({ cutSegments, getFrameCount }) => cutSegments.map(({ start, end, name }) => [
|
||||
safeFormatFrameCount({ seconds: start, getFrameCount }),
|
||||
safeFormatFrameCount({ seconds: end, getFrameCount }),
|
||||
name,
|
||||
]);
|
||||
|
||||
export async function saveCsvHuman(path, cutSegments) {
|
||||
const str = await csvStringifyAsync(formatSegmentsTimes(cutSegments));
|
||||
await fs.writeFile(path, str);
|
||||
await fs.writeFile(path, await formatCsvHuman(cutSegments));
|
||||
}
|
||||
|
||||
export async function saveCsvFrames({ path, cutSegments, getFrameCount }) {
|
||||
const str = await csvStringifyAsync(formatSegmentsFrameCounts({ cutSegments, getFrameCount }));
|
||||
await fs.writeFile(path, str);
|
||||
await fs.writeFile(path, await formatCsvFrames({ cutSegments, getFrameCount }));
|
||||
}
|
||||
|
||||
export async function saveTsv(path, cutSegments) {
|
||||
const str = await csvStringifyAsync(formatSegmentsTimes(cutSegments), { delimiter: '\t' });
|
||||
await fs.writeFile(path, str);
|
||||
await fs.writeFile(path, await formatTsv(cutSegments));
|
||||
}
|
||||
|
||||
export async function saveLlcProject({ savePath, filePath, cutSegments }) {
|
||||
|
@ -84,8 +64,9 @@ export async function loadLlcProject(path) {
|
|||
return JSON5.parse(await fs.readFile(path));
|
||||
}
|
||||
|
||||
export async function readEdlFile({ type, path }) {
|
||||
if (type === 'csv') return loadCsv(path);
|
||||
export async function readEdlFile({ type, path, getTimeFromFrameNum }) {
|
||||
if (type === 'csv') return loadCsvSeconds(path);
|
||||
if (type === 'csv-frames') return loadCsvFrames(path, getTimeFromFrameNum);
|
||||
if (type === 'xmeml') return loadXmeml(path);
|
||||
if (type === 'cue') return loadCue(path);
|
||||
if (type === 'pbf') return loadPbf(path);
|
||||
|
@ -97,11 +78,11 @@ export async function readEdlFile({ type, path }) {
|
|||
throw new Error('Invalid EDL type');
|
||||
}
|
||||
|
||||
export async function askForEdlImport(type) {
|
||||
export async function askForEdlImport({ type, getTimeFromFrameNum }) {
|
||||
if (type === 'youtube') return askForYouTubeInput();
|
||||
|
||||
let filters;
|
||||
if (type === 'csv') filters = [{ name: i18n.t('CSV files'), extensions: ['csv'] }];
|
||||
if (type === 'csv' || type === 'csv-frames') filters = [{ name: i18n.t('CSV files'), extensions: ['csv'] }];
|
||||
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'] }];
|
||||
|
@ -110,7 +91,7 @@ export async function askForEdlImport(type) {
|
|||
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openFile'], filters });
|
||||
if (canceled || filePaths.length < 1) return [];
|
||||
return readEdlFile({ type, path: filePaths[0] });
|
||||
return readEdlFile({ type, path: filePaths[0], getTimeFromFrameNum });
|
||||
}
|
||||
|
||||
export async function exportEdlFile({ type, cutSegments, filePath, getFrameCount }) {
|
||||
|
|
Ładowanie…
Reference in New Issue