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:
parent
1ab1ab3097
commit
eb8f7af5e0
@ -27,6 +27,7 @@ const defaults = {
|
||||
segmentsToChapters: false,
|
||||
preserveMetadataOnMerge: false,
|
||||
simpleMode: true,
|
||||
outSegTemplate: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
|
56
src/App.jsx
56
src/App.jsx
@ -49,7 +49,8 @@ import { saveCsv, saveTsv, loadCsv, loadXmeml, loadCue, loadPbf, saveCsvHuman }
|
||||
import {
|
||||
getOutPath, formatDuration, toast, errorToast, showFfmpegFail, setFileNameTitle, getOutDir, withBlur,
|
||||
checkDirWriteAccess, dirExists, openDirToast, isMasBuild, isStoreBuild, dragPreventer, doesPlayerSupportFile,
|
||||
isDurationValid, isWindows,
|
||||
isDurationValid, isWindows, filenamify, getOutFileExtension, generateSegFileName, defaultOutSegTemplate,
|
||||
hasDuplicates,
|
||||
} from './util';
|
||||
import { askForOutDir, askForImportChapters, createNumSegments, createFixedDurationSegments, promptTimeOffset, askForHtml5ifySpeed, askForYouTubeInput, askForFileOpenAction, confirmExtractAllStreamsDialog, cleanupFilesDialog, showDiskFull } from './dialogs';
|
||||
import { openSendReportDialog } from './reporting';
|
||||
@ -64,7 +65,7 @@ const isDev = window.require('electron-is-dev');
|
||||
const electron = window.require('electron'); // eslint-disable-line
|
||||
const trash = window.require('trash');
|
||||
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;
|
||||
|
||||
@ -211,6 +212,10 @@ const App = memo(() => {
|
||||
useEffect(() => safeSetConfig('preserveMetadataOnMerge', preserveMetadataOnMerge), [preserveMetadataOnMerge]);
|
||||
const [simpleMode, setSimpleMode] = useState(configStore.get('simpleMode'));
|
||||
useEffect(() => safeSetConfig('simpleMode', simpleMode), [simpleMode]);
|
||||
const [outSegTemplate, setOutSegTemplate] = useState(configStore.get('outSegTemplate'));
|
||||
useEffect(() => safeSetConfig('outSegTemplate', outSegTemplate), [outSegTemplate]);
|
||||
|
||||
const outSegTemplateOrDefault = outSegTemplate || defaultOutSegTemplate;
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language || fallbackLng).catch(console.error);
|
||||
@ -982,6 +987,33 @@ const App = memo(() => {
|
||||
const outSegments = useMemo(() => (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 state = {
|
||||
filePath,
|
||||
@ -1035,24 +1067,30 @@ const App = memo(() => {
|
||||
setStreamsSelectorShown(false);
|
||||
setExportConfirmVisible(false);
|
||||
|
||||
const outSegmentsWithOrder = outSegments.map((s, order) => ({ ...s, order }));
|
||||
const filteredOutSegments = exportSingle ? [outSegmentsWithOrder[currentSegIndexSafe]] : outSegmentsWithOrder;
|
||||
const filteredOutSegments = exportSingle ? [outSegments[currentSegIndexSafe]] : outSegments;
|
||||
|
||||
try {
|
||||
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; })();
|
||||
const outFiles = await cutMultiple({
|
||||
customOutDir,
|
||||
outputDir,
|
||||
filePath,
|
||||
outFormat: fileFormat,
|
||||
isCustomFormatSelected,
|
||||
videoDuration: duration,
|
||||
rotation: isRotationSet ? effectiveRotation : undefined,
|
||||
copyFileStreams,
|
||||
keyframeCut,
|
||||
invertCutSegments,
|
||||
segments: filteredOutSegments,
|
||||
segmentsFileNames: outSegFileNames,
|
||||
onProgress: setCutProgress,
|
||||
appendFfmpegCommandLog,
|
||||
shortestFlag,
|
||||
@ -1118,7 +1156,7 @@ const App = memo(() => {
|
||||
setWorking();
|
||||
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 () => {
|
||||
if (working || !filePath) return;
|
||||
@ -2252,7 +2290,7 @@ const App = memo(() => {
|
||||
</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
|
||||
visible={helpVisible}
|
||||
|
@ -12,6 +12,8 @@ import MergeExportButton from './components/MergeExportButton';
|
||||
import PreserveMovDataButton from './components/PreserveMovDataButton';
|
||||
import MovFastStartButton from './components/MovFastStartButton';
|
||||
import ToggleExportConfirm from './components/ToggleExportConfirm';
|
||||
import OutSegTemplateEditor from './components/OutSegTemplateEditor';
|
||||
import HighlightedText from './components/HighlightedText';
|
||||
|
||||
import { withBlur, toast, getSegColors } from './util';
|
||||
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%' };
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
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 HelpIcon = ({ onClick }) => <IoIosHelpCircle size={20} role="button" onClick={withBlur(onClick)} style={{ cursor: 'pointer', verticalAlign: 'middle', marginLeft: 5 }} />;
|
||||
|
||||
const ExportConfirm = memo(({
|
||||
autoMerge, areWeCutting, outSegments, visible, onClosePress, onExportConfirm, keyframeCut, toggleKeyframeCut,
|
||||
toggleAutoMerge, renderOutFmt, preserveMovData, togglePreserveMovData, movFastStart, toggleMovFastStart, avoidNegativeTs, setAvoidNegativeTs,
|
||||
changeOutDir, outputDir, numStreamsTotal, numStreamsToCopy, setStreamsSelectorShown, currentSegIndex, invertCutSegments,
|
||||
exportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, toggleSegmentsToChapters, outFormat,
|
||||
preserveMetadataOnMerge, togglePreserveMetadataOnMerge,
|
||||
preserveMetadataOnMerge, togglePreserveMetadataOnMerge, outSegTemplate, setOutSegTemplate, generateOutSegFileNames,
|
||||
filePath, currentSegIndexSafe, isOutSegFileNamesValid,
|
||||
}) => {
|
||||
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') });
|
||||
}
|
||||
|
||||
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() {
|
||||
// https://ffmpeg.org/ffmpeg-all.html#Format-Options
|
||||
const texts = {
|
||||
@ -96,6 +100,8 @@ const ExportConfirm = memo(({
|
||||
return segBgColor;
|
||||
}
|
||||
|
||||
const outSegTemplateHelpIcon = <HelpIcon onClick={onOutSegTemplateHelpPress} />;
|
||||
|
||||
// https://stackoverflow.com/questions/33454533/cant-scroll-to-top-of-flex-item-that-is-overflowing-container
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@ -118,12 +124,17 @@ const ExportConfirm = memo(({
|
||||
<HelpIcon onClick={onOutFmtHelpPress} />
|
||||
</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} />
|
||||
</li>
|
||||
<li>
|
||||
{t('Save output to path:')} <span role="button" onClick={changeOutDir} style={outDirStyle}>{outputDir}</span>
|
||||
</li>
|
||||
{(outSegments.length === 1 || !autoMerge) && (
|
||||
<li>
|
||||
<OutSegTemplateEditor filePath={filePath} helpIcon={outSegTemplateHelpIcon} outSegTemplate={outSegTemplate} setOutSegTemplate={setOutSegTemplate} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} isOutSegFileNamesValid={isOutSegFileNamesValid} />
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<h3>{t('Advanced options')}</h3>
|
||||
|
6
src/components/HighlightedText.jsx
Normal file
6
src/components/HighlightedText.jsx
Normal 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;
|
90
src/components/OutSegTemplateEditor.jsx
Normal file
90
src/components/OutSegTemplateEditor.jsx
Normal 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;
|
@ -7,10 +7,10 @@ import moment from 'moment';
|
||||
import i18n from 'i18next';
|
||||
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 { join, extname } = window.require('path');
|
||||
const { join } = window.require('path');
|
||||
const fileType = window.require('file-type');
|
||||
const readChunk = window.require('read-chunk');
|
||||
const readline = window.require('readline');
|
||||
@ -88,15 +88,6 @@ export function isCuttingEnd(cutTo, duration) {
|
||||
return cutTo < duration;
|
||||
}
|
||||
|
||||
function getExtensionForFormat(format) {
|
||||
const ext = {
|
||||
matroska: 'mkv',
|
||||
ipod: 'm4a',
|
||||
}[format];
|
||||
|
||||
return ext || format;
|
||||
}
|
||||
|
||||
function getIntervalAroundTime(time, window) {
|
||||
return {
|
||||
from: Math.max(time - window / 2, 0),
|
||||
@ -296,15 +287,11 @@ async function cut({
|
||||
await transferTimestamps(filePath, outPath);
|
||||
}
|
||||
|
||||
function getOutFileExtension({ isCustomFormatSelected, outFormat, filePath }) {
|
||||
return isCustomFormatSelected ? `.${getExtensionForFormat(outFormat)}` : extname(filePath);
|
||||
}
|
||||
|
||||
export async function cutMultiple({
|
||||
customOutDir, filePath, segments, videoDuration, rotation,
|
||||
onProgress, keyframeCut, copyFileStreams, outFormat, isCustomFormatSelected,
|
||||
outputDir, filePath, segments, segmentsFileNames, videoDuration, rotation,
|
||||
onProgress, keyframeCut, copyFileStreams, outFormat,
|
||||
appendFfmpegCommandLog, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, avoidNegativeTs,
|
||||
customTagsByFile, customTagsByStreamId, invertCutSegments,
|
||||
customTagsByFile, customTagsByStreamId,
|
||||
}) {
|
||||
console.log('customTagsByFile', customTagsByFile);
|
||||
console.log('customTagsByStreamId', customTagsByStreamId);
|
||||
@ -317,21 +304,11 @@ export async function cutMultiple({
|
||||
|
||||
const outFiles = [];
|
||||
|
||||
let i = 0;
|
||||
// eslint-disable-next-line no-restricted-syntax,no-unused-vars
|
||||
for (const { start, end, name, order } of segments) {
|
||||
const cutFromStr = formatDuration({ seconds: start, fileNameFriendly: true });
|
||||
const cutToStr = formatDuration({ seconds: end, fileNameFriendly: true });
|
||||
let segNamePart = '';
|
||||
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);
|
||||
for (const [i, { start, end }] of segments.entries()) {
|
||||
const fileName = segmentsFileNames[i];
|
||||
|
||||
const outPath = join(outputDir, fileName);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await cut({
|
||||
@ -357,8 +334,6 @@ export async function cutMultiple({
|
||||
});
|
||||
|
||||
outFiles.push(outPath);
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return outFiles;
|
||||
|
24
src/util.js
24
src/util.js
@ -1,6 +1,7 @@
|
||||
import padStart from 'lodash/padStart';
|
||||
import Swal from 'sweetalert2';
|
||||
import i18n from 'i18next';
|
||||
import lodashTemplate from 'lodash/template';
|
||||
|
||||
import randomColor from './random-color';
|
||||
|
||||
@ -166,3 +167,26 @@ export const isDurationValid = (duration) => Number.isFinite(duration) && durati
|
||||
const platform = os.platform();
|
||||
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user