1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-22 18:32:34 +01:00

Implement custom file name templates #96

This commit is contained in:
Mikael Finstad 2021-01-23 19:02:33 +01:00
parent 1ab1ab3097
commit eb8f7af5e0
7 changed files with 194 additions and 49 deletions

View File

@ -27,6 +27,7 @@ const defaults = {
segmentsToChapters: false, segmentsToChapters: false,
preserveMetadataOnMerge: false, preserveMetadataOnMerge: false,
simpleMode: true, simpleMode: true,
outSegTemplate: undefined,
}, },
}; };

View File

@ -49,7 +49,8 @@ import { saveCsv, saveTsv, loadCsv, loadXmeml, loadCue, loadPbf, saveCsvHuman }
import { import {
getOutPath, formatDuration, toast, errorToast, showFfmpegFail, setFileNameTitle, getOutDir, withBlur, getOutPath, formatDuration, toast, errorToast, showFfmpegFail, setFileNameTitle, getOutDir, withBlur,
checkDirWriteAccess, dirExists, openDirToast, isMasBuild, isStoreBuild, dragPreventer, doesPlayerSupportFile, checkDirWriteAccess, dirExists, openDirToast, isMasBuild, isStoreBuild, dragPreventer, doesPlayerSupportFile,
isDurationValid, isWindows, isDurationValid, isWindows, filenamify, getOutFileExtension, generateSegFileName, defaultOutSegTemplate,
hasDuplicates,
} from './util'; } from './util';
import { askForOutDir, askForImportChapters, createNumSegments, createFixedDurationSegments, promptTimeOffset, askForHtml5ifySpeed, askForYouTubeInput, askForFileOpenAction, confirmExtractAllStreamsDialog, cleanupFilesDialog, showDiskFull } from './dialogs'; import { askForOutDir, askForImportChapters, createNumSegments, createFixedDurationSegments, promptTimeOffset, askForHtml5ifySpeed, askForYouTubeInput, askForFileOpenAction, confirmExtractAllStreamsDialog, cleanupFilesDialog, showDiskFull } from './dialogs';
import { openSendReportDialog } from './reporting'; import { openSendReportDialog } from './reporting';
@ -64,7 +65,7 @@ const isDev = window.require('electron-is-dev');
const electron = window.require('electron'); // eslint-disable-line const electron = window.require('electron'); // eslint-disable-line
const trash = window.require('trash'); const trash = window.require('trash');
const { unlink, exists } = window.require('fs-extra'); const { unlink, exists } = window.require('fs-extra');
const { extname, parse: parsePath } = window.require('path'); const { extname, parse: parsePath, sep: pathSep, join: pathJoin, normalize: pathNormalize } = window.require('path');
const { dialog, app } = electron.remote; const { dialog, app } = electron.remote;
@ -211,6 +212,10 @@ const App = memo(() => {
useEffect(() => safeSetConfig('preserveMetadataOnMerge', preserveMetadataOnMerge), [preserveMetadataOnMerge]); useEffect(() => safeSetConfig('preserveMetadataOnMerge', preserveMetadataOnMerge), [preserveMetadataOnMerge]);
const [simpleMode, setSimpleMode] = useState(configStore.get('simpleMode')); const [simpleMode, setSimpleMode] = useState(configStore.get('simpleMode'));
useEffect(() => safeSetConfig('simpleMode', simpleMode), [simpleMode]); useEffect(() => safeSetConfig('simpleMode', simpleMode), [simpleMode]);
const [outSegTemplate, setOutSegTemplate] = useState(configStore.get('outSegTemplate'));
useEffect(() => safeSetConfig('outSegTemplate', outSegTemplate), [outSegTemplate]);
const outSegTemplateOrDefault = outSegTemplate || defaultOutSegTemplate;
useEffect(() => { useEffect(() => {
i18n.changeLanguage(language || fallbackLng).catch(console.error); i18n.changeLanguage(language || fallbackLng).catch(console.error);
@ -982,6 +987,33 @@ const App = memo(() => {
const outSegments = useMemo(() => (invertCutSegments ? inverseCutSegments : apparentCutSegments), const outSegments = useMemo(() => (invertCutSegments ? inverseCutSegments : apparentCutSegments),
[invertCutSegments, inverseCutSegments, apparentCutSegments]); [invertCutSegments, inverseCutSegments, apparentCutSegments]);
const generateOutSegFileNames = useCallback(({ segments = outSegments, template }) => (
segments.map(({ start, end, name = '' }, i) => {
const cutFromStr = formatDuration({ seconds: start, fileNameFriendly: true });
const cutToStr = formatDuration({ seconds: end, fileNameFriendly: true });
const segNum = i + 1;
// https://github.com/mifi/lossless-cut/issues/583
let segSuffix = '';
if (name) segSuffix = `-${filenamify(name)}`;
else if (segments.length > 1) segSuffix = `-seg${segNum}`;
const ext = getOutFileExtension({ isCustomFormatSelected, outFormat: fileFormat, filePath });
const { name: fileNameWithoutExt } = parsePath(filePath);
const generated = generateSegFileName({ template, segSuffix, inputFileNameWithoutExt: fileNameWithoutExt, ext, segNum, segLabel: filenamify(name), cutFrom: cutFromStr, cutTo: cutToStr });
return generated.substr(0, 200); // Just to be sure
})
), [fileFormat, filePath, isCustomFormatSelected, outSegments]);
// TODO improve user feedback
const isOutSegFileNamesValid = useCallback((fileNames) => fileNames.every((fileName) => {
if (!filePath) return false;
const sameAsInputPath = pathNormalize(pathJoin(outputDir, fileName)) === pathNormalize(filePath);
return fileName.length > 0 && !fileName.includes(pathSep) && !sameAsInputPath;
}), [outputDir, filePath]);
const openSendReportDialogWithState = useCallback(async (err) => { const openSendReportDialogWithState = useCallback(async (err) => {
const state = { const state = {
filePath, filePath,
@ -1035,24 +1067,30 @@ const App = memo(() => {
setStreamsSelectorShown(false); setStreamsSelectorShown(false);
setExportConfirmVisible(false); setExportConfirmVisible(false);
const outSegmentsWithOrder = outSegments.map((s, order) => ({ ...s, order })); const filteredOutSegments = exportSingle ? [outSegments[currentSegIndexSafe]] : outSegments;
const filteredOutSegments = exportSingle ? [outSegmentsWithOrder[currentSegIndexSafe]] : outSegmentsWithOrder;
try { try {
setWorking(i18n.t('Exporting')); setWorking(i18n.t('Exporting'));
console.log('outSegTemplateOrDefault', outSegTemplateOrDefault);
let outSegFileNames = generateOutSegFileNames({ segments: filteredOutSegments, template: outSegTemplateOrDefault });
if (!isOutSegFileNamesValid(outSegFileNames) || hasDuplicates(outSegFileNames)) {
console.error('Output segments file name invalid, using default instead', outSegFileNames);
outSegFileNames = generateOutSegFileNames({ segments: filteredOutSegments, template: defaultOutSegTemplate });
}
// throw (() => { const err = new Error('test'); err.code = 'ENOENT'; return err; })(); // throw (() => { const err = new Error('test'); err.code = 'ENOENT'; return err; })();
const outFiles = await cutMultiple({ const outFiles = await cutMultiple({
customOutDir, outputDir,
filePath, filePath,
outFormat: fileFormat, outFormat: fileFormat,
isCustomFormatSelected,
videoDuration: duration, videoDuration: duration,
rotation: isRotationSet ? effectiveRotation : undefined, rotation: isRotationSet ? effectiveRotation : undefined,
copyFileStreams, copyFileStreams,
keyframeCut, keyframeCut,
invertCutSegments,
segments: filteredOutSegments, segments: filteredOutSegments,
segmentsFileNames: outSegFileNames,
onProgress: setCutProgress, onProgress: setCutProgress,
appendFfmpegCommandLog, appendFfmpegCommandLog,
shortestFlag, shortestFlag,
@ -1118,7 +1156,7 @@ const App = memo(() => {
setWorking(); setWorking();
setCutProgress(); setCutProgress();
} }
}, [autoMerge, copyFileStreams, customOutDir, duration, effectiveRotation, exportExtraStreams, ffmpegExperimental, fileFormat, fileFormatData, filePath, handleCutFailed, isCustomFormatSelected, isRotationSet, keyframeCut, mainStreams, nonCopiedExtraStreams, outSegments, outputDir, shortestFlag, working, preserveMovData, movFastStart, avoidNegativeTs, numStreamsToCopy, hideAllNotifications, currentSegIndexSafe, invertCutSegments, autoDeleteMergedSegments, segmentsToChapters, customTagsByFile, customTagsByStreamId, preserveMetadataOnMerge]); }, [working, numStreamsToCopy, outSegments, currentSegIndexSafe, outSegTemplateOrDefault, generateOutSegFileNames, customOutDir, filePath, fileFormat, duration, isRotationSet, effectiveRotation, copyFileStreams, keyframeCut, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, avoidNegativeTs, customTagsByFile, customTagsByStreamId, autoMerge, exportExtraStreams, fileFormatData, mainStreams, hideAllNotifications, outputDir, segmentsToChapters, invertCutSegments, isCustomFormatSelected, autoDeleteMergedSegments, preserveMetadataOnMerge, nonCopiedExtraStreams, handleCutFailed, isOutSegFileNamesValid]);
const onExportPress = useCallback(async () => { const onExportPress = useCallback(async () => {
if (working || !filePath) return; if (working || !filePath) return;
@ -2252,7 +2290,7 @@ const App = memo(() => {
</div> </div>
</motion.div> </motion.div>
<ExportConfirm autoMerge={autoMerge} toggleAutoMerge={toggleAutoMerge} areWeCutting={areWeCutting} outSegments={outSegments} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} keyframeCut={keyframeCut} toggleKeyframeCut={toggleKeyframeCut} renderOutFmt={renderOutFmt} preserveMovData={preserveMovData} togglePreserveMovData={togglePreserveMovData} movFastStart={movFastStart} toggleMovFastStart={toggleMovFastStart} avoidNegativeTs={avoidNegativeTs} setAvoidNegativeTs={setAvoidNegativeTs} changeOutDir={changeOutDir} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} setStreamsSelectorShown={setStreamsSelectorShown} currentSegIndex={currentSegIndexSafe} invertCutSegments={invertCutSegments} exportConfirmEnabled={exportConfirmEnabled} toggleExportConfirmEnabled={toggleExportConfirmEnabled} segmentsToChapters={segmentsToChapters} toggleSegmentsToChapters={toggleSegmentsToChapters} outFormat={fileFormat} preserveMetadataOnMerge={preserveMetadataOnMerge} togglePreserveMetadataOnMerge={togglePreserveMetadataOnMerge} /> <ExportConfirm filePath={filePath} autoMerge={autoMerge} toggleAutoMerge={toggleAutoMerge} areWeCutting={areWeCutting} outSegments={outSegments} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} keyframeCut={keyframeCut} toggleKeyframeCut={toggleKeyframeCut} renderOutFmt={renderOutFmt} preserveMovData={preserveMovData} togglePreserveMovData={togglePreserveMovData} movFastStart={movFastStart} toggleMovFastStart={toggleMovFastStart} avoidNegativeTs={avoidNegativeTs} setAvoidNegativeTs={setAvoidNegativeTs} changeOutDir={changeOutDir} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} setStreamsSelectorShown={setStreamsSelectorShown} currentSegIndex={currentSegIndexSafe} invertCutSegments={invertCutSegments} exportConfirmEnabled={exportConfirmEnabled} toggleExportConfirmEnabled={toggleExportConfirmEnabled} segmentsToChapters={segmentsToChapters} toggleSegmentsToChapters={toggleSegmentsToChapters} outFormat={fileFormat} preserveMetadataOnMerge={preserveMetadataOnMerge} togglePreserveMetadataOnMerge={togglePreserveMetadataOnMerge} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} isOutSegFileNamesValid={isOutSegFileNamesValid} />
<HelpSheet <HelpSheet
visible={helpVisible} visible={helpVisible}

View File

@ -12,6 +12,8 @@ import MergeExportButton from './components/MergeExportButton';
import PreserveMovDataButton from './components/PreserveMovDataButton'; import PreserveMovDataButton from './components/PreserveMovDataButton';
import MovFastStartButton from './components/MovFastStartButton'; import MovFastStartButton from './components/MovFastStartButton';
import ToggleExportConfirm from './components/ToggleExportConfirm'; import ToggleExportConfirm from './components/ToggleExportConfirm';
import OutSegTemplateEditor from './components/OutSegTemplateEditor';
import HighlightedText from './components/HighlightedText';
import { withBlur, toast, getSegColors } from './util'; import { withBlur, toast, getSegColors } from './util';
import { isMov as ffmpegIsMov } from './ffmpeg'; import { isMov as ffmpegIsMov } from './ffmpeg';
@ -35,17 +37,15 @@ const outDirStyle = { background: 'rgb(193, 98, 0)', borderRadius: '.4em', paddi
const warningStyle = { color: '#faa', fontSize: '80%' }; const warningStyle = { color: '#faa', fontSize: '80%' };
// eslint-disable-next-line react/jsx-props-no-spreading const HelpIcon = ({ onClick }) => <IoIosHelpCircle size={20} role="button" onClick={withBlur(onClick)} style={{ cursor: 'pointer', verticalAlign: 'middle', marginLeft: 5 }} />;
const Highlight = ({ children, style, ...props }) => <span {...props} style={{ background: 'rgb(193, 98, 0)', borderRadius: '.4em', padding: '0 .3em', ...style }}>{children}</span>;
const HelpIcon = ({ onClick }) => <IoIosHelpCircle size={20} role="button" onClick={withBlur(onClick)} style={{ verticalAlign: 'middle', marginLeft: 5 }} />;
const ExportConfirm = memo(({ const ExportConfirm = memo(({
autoMerge, areWeCutting, outSegments, visible, onClosePress, onExportConfirm, keyframeCut, toggleKeyframeCut, autoMerge, areWeCutting, outSegments, visible, onClosePress, onExportConfirm, keyframeCut, toggleKeyframeCut,
toggleAutoMerge, renderOutFmt, preserveMovData, togglePreserveMovData, movFastStart, toggleMovFastStart, avoidNegativeTs, setAvoidNegativeTs, toggleAutoMerge, renderOutFmt, preserveMovData, togglePreserveMovData, movFastStart, toggleMovFastStart, avoidNegativeTs, setAvoidNegativeTs,
changeOutDir, outputDir, numStreamsTotal, numStreamsToCopy, setStreamsSelectorShown, currentSegIndex, invertCutSegments, changeOutDir, outputDir, numStreamsTotal, numStreamsToCopy, setStreamsSelectorShown, currentSegIndex, invertCutSegments,
exportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, toggleSegmentsToChapters, outFormat, exportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, toggleSegmentsToChapters, outFormat,
preserveMetadataOnMerge, togglePreserveMetadataOnMerge, preserveMetadataOnMerge, togglePreserveMetadataOnMerge, outSegTemplate, setOutSegTemplate, generateOutSegFileNames,
filePath, currentSegIndexSafe, isOutSegFileNamesValid,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -80,6 +80,10 @@ const ExportConfirm = memo(({
toast.fire({ icon: 'info', timer: 10000, text: i18n.t('When merging, do you want to preserve metadata from your original file? NOTE: This may dramatically increase processing time') }); toast.fire({ icon: 'info', timer: 10000, text: i18n.t('When merging, do you want to preserve metadata from your original file? NOTE: This may dramatically increase processing time') });
} }
function onOutSegTemplateHelpPress() {
toast.fire({ icon: 'info', timer: 10000, text: i18n.t('You can customize the file name of the output segment(s) using special variables.') });
}
function onAvoidNegativeTsHelpPress() { function onAvoidNegativeTsHelpPress() {
// https://ffmpeg.org/ffmpeg-all.html#Format-Options // https://ffmpeg.org/ffmpeg-all.html#Format-Options
const texts = { const texts = {
@ -96,6 +100,8 @@ const ExportConfirm = memo(({
return segBgColor; return segBgColor;
} }
const outSegTemplateHelpIcon = <HelpIcon onClick={onOutSegTemplateHelpPress} />;
// https://stackoverflow.com/questions/33454533/cant-scroll-to-top-of-flex-item-that-is-overflowing-container // https://stackoverflow.com/questions/33454533/cant-scroll-to-top-of-flex-item-that-is-overflowing-container
return ( return (
<AnimatePresence> <AnimatePresence>
@ -118,12 +124,17 @@ const ExportConfirm = memo(({
<HelpIcon onClick={onOutFmtHelpPress} /> <HelpIcon onClick={onOutFmtHelpPress} />
</li> </li>
<li> <li>
<Trans>Input has {{ numStreamsTotal }} tracks - <Highlight style={{ cursor: 'pointer' }} onClick={() => setStreamsSelectorShown(true)}>Keeping {{ numStreamsToCopy }} tracks</Highlight></Trans> <Trans>Input has {{ numStreamsTotal }} tracks - <HighlightedText style={{ cursor: 'pointer' }} onClick={() => setStreamsSelectorShown(true)}>Keeping {{ numStreamsToCopy }} tracks</HighlightedText></Trans>
<HelpIcon onClick={onTracksHelpPress} /> <HelpIcon onClick={onTracksHelpPress} />
</li> </li>
<li> <li>
{t('Save output to path:')} <span role="button" onClick={changeOutDir} style={outDirStyle}>{outputDir}</span> {t('Save output to path:')} <span role="button" onClick={changeOutDir} style={outDirStyle}>{outputDir}</span>
</li> </li>
{(outSegments.length === 1 || !autoMerge) && (
<li>
<OutSegTemplateEditor filePath={filePath} helpIcon={outSegTemplateHelpIcon} outSegTemplate={outSegTemplate} setOutSegTemplate={setOutSegTemplate} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} isOutSegFileNamesValid={isOutSegFileNamesValid} />
</li>
)}
</ul> </ul>
<h3>{t('Advanced options')}</h3> <h3>{t('Advanced options')}</h3>

View File

@ -0,0 +1,6 @@
import React, { memo } from 'react';
// eslint-disable-next-line react/jsx-props-no-spreading
const HighlightedText = memo(({ children, style, ...props }) => <span {...props} style={{ background: 'rgb(193, 98, 0)', borderRadius: '.4em', padding: '0 .3em', ...style }}>{children}</span>);
export default HighlightedText;

View File

@ -0,0 +1,90 @@
import React, { memo, useState, useEffect } from 'react';
import { useDebounce } from 'use-debounce';
import { useTranslation } from 'react-i18next';
import { Button, Alert } from 'evergreen-ui';
import Swal from 'sweetalert2';
import withReactContent from 'sweetalert2-react-content';
import HighlightedText from './HighlightedText';
import { defaultOutSegTemplate } from '../util';
const ReactSwal = withReactContent(Swal);
const OutSegTemplateEditor = memo(({ helpIcon, outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe, isOutSegFileNamesValid }) => {
const [text, setText] = useState(outSegTemplate);
const [debouncedText] = useDebounce(text, 500);
const [validText, setValidText] = useState();
const [error, setError] = useState();
const [outSegFileNames, setOutSegFileNames] = useState();
const [shown, setShown] = useState();
const { t } = useTranslation();
useEffect(() => {
if (debouncedText == null) return;
try {
const generatedOutSegFileNames = generateOutSegFileNames({ template: debouncedText });
setOutSegFileNames(generatedOutSegFileNames);
const isOutSegTemplateValid = isOutSegFileNamesValid(generatedOutSegFileNames);
if (!isOutSegTemplateValid) {
setError(t('This template will result in invalid file names'));
setValidText();
return;
}
setValidText(debouncedText);
setError();
} catch (err) {
console.error(err);
setValidText();
setError(err.message);
}
}, [debouncedText, generateOutSegFileNames, isOutSegFileNamesValid, t]);
const onAllSegmentsPreviewPress = () => ReactSwal.fire({ title: t('Resulting segment file names'), html: <div style={{ textAlign: 'left', overflowY: 'auto', maxHeight: 400 }}>{outSegFileNames.map((f) => <div key={f} style={{ marginBottom: 7 }}>{f}</div>)}</div> });
useEffect(() => {
if (validText != null) setOutSegTemplate(validText);
}, [validText, setOutSegTemplate]);
function reset() {
setOutSegTemplate(defaultOutSegTemplate);
setText(defaultOutSegTemplate);
}
function onToggleClick() {
if (!shown) setShown(true);
else if (error == null) setShown(false);
}
return (
<>
<div>
<span role="button" onClick={onToggleClick} style={{ cursor: 'pointer' }}>
{t('Output name(s):')} {outSegFileNames != null && <HighlightedText style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{outSegFileNames[currentSegIndexSafe]}</HighlightedText>}
</span>
{helpIcon}
</div>
{shown && (
<>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 5, marginTop: 5 }}>
<input type="text" style={{ flexGrow: 1, fontFamily: 'inherit', fontSize: '.8em' }} onChange={(e) => setText(e.target.value)} value={text} autoComplete="off" autoCapitalize="off" autoCorrect="off" />
{outSegFileNames && <Button height={20} onClick={onAllSegmentsPreviewPress} style={{ marginLeft: 5 }}>{t('Preview')}</Button>}
<Button height={20} onClick={reset} style={{ marginLeft: 5 }} intent="danger">{t('Reset')}</Button>
<Button height={20} onClick={onToggleClick} style={{ marginLeft: 5 }} intent="success">{t('Close')}</Button>
</div>
<div>
{error != null && <Alert intent="danger" appearance="card">{`There is an error in the file name template: ${error}`}</Alert>}
{/* eslint-disable-next-line no-template-curly-in-string */}
<div style={{ fontSize: '.8em', color: 'rgba(255,255,255,0.7)' }}>{'Variables: ${FILENAME} ${CUT_FROM} ${CUT_TO} ${SEG_NUM} ${SEG_LABEL} ${SEG_SUFFIX} ${EXT}'}</div>
</div>
</>
)}
</>
);
});
export default OutSegTemplateEditor;

View File

@ -7,10 +7,10 @@ import moment from 'moment';
import i18n from 'i18next'; import i18n from 'i18next';
import Timecode from 'smpte-timecode'; import Timecode from 'smpte-timecode';
import { formatDuration, getOutPath, getOutDir, transferTimestamps, filenamify, isDurationValid } from './util'; import { getOutPath, getOutDir, transferTimestamps, isDurationValid, getExtensionForFormat, getOutFileExtension } from './util';
const execa = window.require('execa'); const execa = window.require('execa');
const { join, extname } = window.require('path'); const { join } = window.require('path');
const fileType = window.require('file-type'); const fileType = window.require('file-type');
const readChunk = window.require('read-chunk'); const readChunk = window.require('read-chunk');
const readline = window.require('readline'); const readline = window.require('readline');
@ -88,15 +88,6 @@ export function isCuttingEnd(cutTo, duration) {
return cutTo < duration; return cutTo < duration;
} }
function getExtensionForFormat(format) {
const ext = {
matroska: 'mkv',
ipod: 'm4a',
}[format];
return ext || format;
}
function getIntervalAroundTime(time, window) { function getIntervalAroundTime(time, window) {
return { return {
from: Math.max(time - window / 2, 0), from: Math.max(time - window / 2, 0),
@ -296,15 +287,11 @@ async function cut({
await transferTimestamps(filePath, outPath); await transferTimestamps(filePath, outPath);
} }
function getOutFileExtension({ isCustomFormatSelected, outFormat, filePath }) {
return isCustomFormatSelected ? `.${getExtensionForFormat(outFormat)}` : extname(filePath);
}
export async function cutMultiple({ export async function cutMultiple({
customOutDir, filePath, segments, videoDuration, rotation, outputDir, filePath, segments, segmentsFileNames, videoDuration, rotation,
onProgress, keyframeCut, copyFileStreams, outFormat, isCustomFormatSelected, onProgress, keyframeCut, copyFileStreams, outFormat,
appendFfmpegCommandLog, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, avoidNegativeTs, appendFfmpegCommandLog, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, avoidNegativeTs,
customTagsByFile, customTagsByStreamId, invertCutSegments, customTagsByFile, customTagsByStreamId,
}) { }) {
console.log('customTagsByFile', customTagsByFile); console.log('customTagsByFile', customTagsByFile);
console.log('customTagsByStreamId', customTagsByStreamId); console.log('customTagsByStreamId', customTagsByStreamId);
@ -317,21 +304,11 @@ export async function cutMultiple({
const outFiles = []; const outFiles = [];
let i = 0;
// eslint-disable-next-line no-restricted-syntax,no-unused-vars // eslint-disable-next-line no-restricted-syntax,no-unused-vars
for (const { start, end, name, order } of segments) { for (const [i, { start, end }] of segments.entries()) {
const cutFromStr = formatDuration({ seconds: start, fileNameFriendly: true }); const fileName = segmentsFileNames[i];
const cutToStr = formatDuration({ seconds: end, fileNameFriendly: true });
let segNamePart = ''; const outPath = join(outputDir, fileName);
if (!invertCutSegments) {
if (name) segNamePart = `-${filenamify(name)}`;
// https://github.com/mifi/lossless-cut/issues/583
else if (segments.length > 1) segNamePart = `-seg${order + 1}`;
}
const cutSpecification = `${cutFromStr}-${cutToStr}${segNamePart}`.substr(0, 200);
const ext = getOutFileExtension({ isCustomFormatSelected, outFormat, filePath });
const fileName = `${cutSpecification}${ext}`;
const outPath = getOutPath(customOutDir, filePath, fileName);
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await cut({ await cut({
@ -357,8 +334,6 @@ export async function cutMultiple({
}); });
outFiles.push(outPath); outFiles.push(outPath);
i += 1;
} }
return outFiles; return outFiles;

View File

@ -1,6 +1,7 @@
import padStart from 'lodash/padStart'; import padStart from 'lodash/padStart';
import Swal from 'sweetalert2'; import Swal from 'sweetalert2';
import i18n from 'i18next'; import i18n from 'i18next';
import lodashTemplate from 'lodash/template';
import randomColor from './random-color'; import randomColor from './random-color';
@ -166,3 +167,26 @@ export const isDurationValid = (duration) => Number.isFinite(duration) && durati
const platform = os.platform(); const platform = os.platform();
export const isWindows = platform === 'win32'; export const isWindows = platform === 'win32';
export function getExtensionForFormat(format) {
const ext = {
matroska: 'mkv',
ipod: 'm4a',
}[format];
return ext || format;
}
export function getOutFileExtension({ isCustomFormatSelected, outFormat, filePath }) {
return isCustomFormatSelected ? `.${getExtensionForFormat(outFormat)}` : path.extname(filePath);
}
// eslint-disable-next-line no-template-curly-in-string
export const defaultOutSegTemplate = '${FILENAME}-${CUT_FROM}-${CUT_TO}${SEG_SUFFIX}${EXT}';
export function generateSegFileName({ template, inputFileNameWithoutExt, segSuffix, ext, segNum, segLabel, cutFrom, cutTo }) {
const compiled = lodashTemplate(template);
return compiled({ FILENAME: inputFileNameWithoutExt, SEG_SUFFIX: segSuffix, EXT: ext, SEG_NUM: segNum, SEG_LABEL: segLabel, CUT_FROM: cutFrom, CUT_TO: cutTo });
}
export const hasDuplicates = (arr) => new Set(arr).size !== arr.length;