Add "Keep original chapters" option to merge files

pull/2774/head
thematuu 6 months ago
parent 3b0ef522e0
commit f9dffa03f3

@ -47,6 +47,7 @@ export interface Config {
preserveChapters: boolean,
preserveMetadata: PreserveMetadata,
preserveMetadataOnMerge: boolean,
keepOriginalChaptersOnMerge: boolean,
preserveMovData: boolean,
movFastStart: boolean,
avoidNegativeTs: AvoidNegativeTs,

@ -121,6 +121,7 @@ const defaults: Config = {
preserveChapters: true,
preserveMetadata: 'default',
preserveMetadataOnMerge: false,
keepOriginalChaptersOnMerge: false,
preserveMovData: false,
movFastStart: true,
avoidNegativeTs: 'make_zero',

@ -59,7 +59,7 @@ import {
readFileFfprobeMeta, getDefaultOutFormat,
setCustomFfPath as ffmpegSetCustomFfPath,
isIphoneHevc, isProblematicAvc1, tryMapChaptersToEdl,
getTimecodeFromStreams, createChaptersFromSegments,
getTimecodeFromStreams, createChaptersFromSegments, createChaptersFromOriginalFiles,
RefuseOverwriteError, extractSubtitleTrackToSegments,
mapRecommendedDefaultFormat,
getFfCommandLine,
@ -180,7 +180,7 @@ function App() {
const [selectedBatchFiles, setSelectedBatchFiles] = useState<string[]>([]);
const allUserSettings = useUserSettingsRoot();
const { captureFormat, customOutDir, keyframeCut, preserveMetadata, preserveMetadataOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, autoExportExtraStreams, askBeforeClose, enableImportChapters, enableAskForFileOpenAction, playbackVolume, autoSaveProjectFile, wheelSensitivity, waveformHeight, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, hideOsNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, segmentsToChapters, simpleMode, cutFileTemplate, cutMergedFileTemplate, mergedFileTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, safeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, darkMode, preferStrongColors, outputFileNameMinZeroPadding, cutFromAdjustmentFrames, cutToAdjustmentFrames, waveformMode: waveformModePreference, thumbnailsEnabled, keyframesEnabled, reducedMotion } = allUserSettings.settings;
const { captureFormat, customOutDir, keyframeCut, preserveMetadata, preserveMetadataOnMerge, keepOriginalChaptersOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, autoExportExtraStreams, askBeforeClose, enableImportChapters, enableAskForFileOpenAction, playbackVolume, autoSaveProjectFile, wheelSensitivity, waveformHeight, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, hideOsNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, segmentsToChapters, simpleMode, cutFileTemplate, cutMergedFileTemplate, mergedFileTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, safeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, darkMode, preferStrongColors, outputFileNameMinZeroPadding, cutFromAdjustmentFrames, cutToAdjustmentFrames, waveformMode: waveformModePreference, thumbnailsEnabled, keyframesEnabled, reducedMotion } = allUserSettings.settings;
const { setCaptureFormat, setCustomOutDir, setKeyframeCut, setPlaybackVolume, setExportConfirmEnabled, setSimpleMode, setOutFormatLocked, setSafeOutputFileName, setKeyBindings, resetKeyBindings, setStoreProjectInWorkingDir, setCleanupChoices, toggleDarkMode, setWaveformMode, setThumbnailsEnabled, setKeyframesEnabled, prefersReducedMotion } = allUserSettings;
const [showAdvancedSettings, setShowAdvancedSettings] = useState(!simpleMode);
@ -914,7 +914,9 @@ function App() {
const outPath = getOutPath({ customOutDir, filePath: firstPath, fileName });
let chaptersFromSegments: Awaited<ReturnType<typeof createChaptersFromSegments>>;
if (segmentsToChapters) {
if (keepOriginalChaptersOnMerge) {
chaptersFromSegments = await createChaptersFromOriginalFiles({ paths, createChapterForFilesWithoutChapters: segmentsToChapters });
} else if (segmentsToChapters) {
const chapterNames = paths.map((path) => parsePath(path).name);
chaptersFromSegments = await createChaptersFromSegments({ segmentPaths: paths, chapterNames });
}
@ -971,7 +973,7 @@ function App() {
setWorking(undefined);
setProgress(undefined);
}
}, [workingRef, ensureWritableOutDir, customOutDir, setWorking, segmentsToChapters, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch, enableOverwriteOutput, hideAllNotifications, t, showOsNotification, openConcatFinishedDialog, handleConcatFailed]);
}, [workingRef, ensureWritableOutDir, customOutDir, setWorking, segmentsToChapters, keepOriginalChaptersOnMerge, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch, enableOverwriteOutput, hideAllNotifications, t, showOsNotification, openConcatFinishedDialog, handleConcatFailed]);
const cleanupFiles = useCallback(async (cleanupChoices2: CleanupChoicesType) => {
// Store paths before we reset state

@ -55,7 +55,7 @@ function ConcatDialog({ isShown, onHide, paths, mergedFileTemplate, generateMerg
onOutputFormatUserChange: (newFormat: string) => void,
}) {
const { t } = useTranslation();
const { preserveMovData, setPreserveMovData, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, customOutDir, simpleMode, setMergedFileTemplate, outFormatLocked, changeOutDir } = useUserSettings();
const { preserveMovData, setPreserveMovData, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, keepOriginalChaptersOnMerge, setKeepOriginalChaptersOnMerge, customOutDir, simpleMode, setMergedFileTemplate, outFormatLocked, changeOutDir } = useUserSettings();
const [includeAllStreams, setIncludeAllStreams] = useState(false);
const [allFilesMeta, setAllFilesMeta] = useState<Record<string, { ffprobeMeta: FileFfprobeMeta, stats: FileStats }>>({});
@ -314,6 +314,8 @@ function ConcatDialog({ isShown, onHide, paths, mergedFileTemplate, generateMerg
<Checkbox checked={segmentsToChapters} onCheckedChange={(checked) => setSegmentsToChapters(checked === true)} label={t('Create chapters from merged segments? (slow)')} />
<Checkbox checked={keepOriginalChaptersOnMerge} onCheckedChange={(checked) => setKeepOriginalChaptersOnMerge(checked === true)} label={t('Keep original chapters')} />
<Checkbox checked={alwaysConcatMultipleFiles} onCheckedChange={(checked) => setAlwaysConcatMultipleFiles(checked === true)} label={t('Always open this dialog when opening multiple files')} />
<Checkbox checked={clearBatchFilesAfterConcat} onCheckedChange={(checked) => setClearBatchFilesAfterConcat(checked === true)} label={t('Clear batch file list after merge')} />

@ -232,6 +232,35 @@ export async function createChaptersFromSegments({ segmentPaths, chapterNames }:
}
}
export async function createChaptersFromOriginalFiles({ paths, createChapterForFilesWithoutChapters }: { paths: string[], createChapterForFilesWithoutChapters: boolean }) {
const { parse } = window.require('path') as { parse: (p: string) => { name: string } };
try {
const mergedChapters: { start: number, end: number, name: string | undefined }[] = [];
let offset = 0;
for (const path of paths) {
const meta = await readFileFfprobeMeta(path);
const duration = (await getDuration(path)) ?? (meta.format?.duration != null ? parseFloat(meta.format.duration) : 0);
const chapters = meta.chapters ?? [];
if (chapters.length > 0) {
for (const ch of chapters) {
mergedChapters.push({
start: offset + parseFloat(ch.start_time),
end: offset + parseFloat(ch.end_time),
name: ch.tags?.title ?? undefined,
});
}
} else if (createChapterForFilesWithoutChapters) {
mergedChapters.push({ start: offset, end: offset + duration, name: parse(path).name });
}
offset += duration;
}
return mergedChapters.length > 0 ? mergedChapters : undefined;
} catch (err) {
console.error('Failed to create chapters from original files', err);
return undefined;
}
}
/**
* Some of the detected input formats are not the same as the muxer name used for encoding.
* Therefore we have to map between detected input format and encode format

@ -62,6 +62,8 @@ export default function useUserSettingsRoot() {
useEffect(() => safeSetConfig({ preserveMetadata }), [preserveMetadata]);
const [preserveMetadataOnMerge, setPreserveMetadataOnMerge] = useState(safeGetConfigInitial('preserveMetadataOnMerge'));
useEffect(() => safeSetConfig({ preserveMetadataOnMerge }), [preserveMetadataOnMerge]);
const [keepOriginalChaptersOnMerge, setKeepOriginalChaptersOnMerge] = useState(safeGetConfigInitial('keepOriginalChaptersOnMerge'));
useEffect(() => safeSetConfig({ keepOriginalChaptersOnMerge }), [keepOriginalChaptersOnMerge]);
const [preserveMovData, setPreserveMovData] = useState(safeGetConfigInitial('preserveMovData'));
useEffect(() => safeSetConfig({ preserveMovData }), [preserveMovData]);
const [preserveChapters, setPreserveChapters] = useState(safeGetConfigInitial('preserveChapters'));
@ -229,6 +231,7 @@ export default function useUserSettingsRoot() {
keyframeCut,
preserveMetadata,
preserveMetadataOnMerge,
keepOriginalChaptersOnMerge,
preserveMovData,
preserveChapters,
movFastStart,
@ -304,6 +307,7 @@ export default function useUserSettingsRoot() {
setKeyframeCut,
setPreserveMetadata,
setPreserveMetadataOnMerge,
setKeepOriginalChaptersOnMerge,
setPreserveMovData,
setPreserveChapters,
setMovFastStart,

Loading…
Cancel
Save