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,
|
segmentsToChapters: false,
|
||||||
preserveMetadataOnMerge: false,
|
preserveMetadataOnMerge: false,
|
||||||
simpleMode: true,
|
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 {
|
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}
|
||||||
|
@ -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>
|
||||||
|
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 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;
|
||||||
|
24
src/util.js
24
src/util.js
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user