From e9e427772b98d2fce79abd3edf6680651e593b3f Mon Sep 17 00:00:00 2001 From: Speck Pratt Date: Wed, 22 Apr 2026 07:08:15 -0400 Subject: [PATCH] Make marker preview duration ceiling configurable Replaces the hardcoded maxMarkerPreviewDuration = 20 constant in pkg/scene/generate/marker_preview.go with a configurable setting on ConfigGeneral, mirroring the PreviewSegments wiring pattern. - Default 20 preserves existing behavior for all installs - Positive N sets the ceiling in seconds - 0 (or any value <= 0) disables the ceiling, honoring explicit endSeconds verbatim - Adds logger.Warnf for non-positive intervals to aid diagnosis of imported or malformed marker data - Incidentally prevents zero/negative-duration ffmpeg calls that were possible under the previous logic Context: closed issue #6852 (template non-conformance), Discussion #6851 (mobile-first direction). Co-Authored-By: Claude Opus 4.7 (1M context) --- graphql/schema/types/config.graphql | 4 +++ internal/api/resolver_mutation_configure.go | 1 + internal/manager/config/config.go | 11 ++++++++ internal/manager/task_generate_markers.go | 2 +- pkg/scene/generate/marker_preview.go | 26 ++++++++++++++----- ui/v2.5/graphql/data/config.graphql | 1 + .../Settings/SettingsSystemPanel.tsx | 8 ++++++ ui/v2.5/src/locales/en-GB.json | 2 ++ 8 files changed, 48 insertions(+), 7 deletions(-) diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 5ab7fdfea..c91c5d231 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -101,6 +101,8 @@ input ConfigGeneralInput { previewSegments: Int "Preview segment duration, in seconds" previewSegmentDuration: Float + "Maximum duration (in seconds) for generated marker preview videos. 0 disables the ceiling." + maxMarkerPreviewDuration: Int "Duration of start of video to exclude when generating previews" previewExcludeStart: String "Duration of end of video to exclude when generating previews" @@ -239,6 +241,8 @@ type ConfigGeneralResult { previewSegments: Int! "Preview segment duration, in seconds" previewSegmentDuration: Float! + "Maximum duration (in seconds) for generated marker preview videos. 0 disables the ceiling." + maxMarkerPreviewDuration: Int! "Duration of start of video to exclude when generating previews" previewExcludeStart: String! "Duration of end of video to exclude when generating previews" diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index 3df1c9114..b0c6dcf5f 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -289,6 +289,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen r.setConfigBool(config.PreviewAudio, input.PreviewAudio) r.setConfigInt(config.PreviewSegments, input.PreviewSegments) r.setConfigFloat(config.PreviewSegmentDuration, input.PreviewSegmentDuration) + r.setConfigInt(config.MaxMarkerPreviewDuration, input.MaxMarkerPreviewDuration) r.setConfigString(config.PreviewExcludeStart, input.PreviewExcludeStart) r.setConfigString(config.PreviewExcludeEnd, input.PreviewExcludeEnd) if input.PreviewPreset != nil { diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index 19e263810..05dcf97a4 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -119,6 +119,9 @@ const ( PreviewExcludeEnd = "preview_exclude_end" previewExcludeEndDefault = "0" + MaxMarkerPreviewDuration = "max_marker_preview_duration" + maxMarkerPreviewDurationDefault = 20 + WriteImageThumbnails = "write_image_thumbnails" writeImageThumbnailsDefault = true @@ -1079,6 +1082,13 @@ func (i *Config) GetTranscodeHardwareAcceleration() bool { return i.getBool(TranscodeHardwareAcceleration) } +// GetMaxMarkerPreviewDuration returns the ceiling in seconds applied to +// generated marker preview videos when the marker has an explicit end time. +// Any value <= 0 disables the ceiling, honoring the marker's end time verbatim. +func (i *Config) GetMaxMarkerPreviewDuration() int { + return i.getInt(MaxMarkerPreviewDuration) +} + func (i *Config) GetMaxTranscodeSize() models.StreamingResolutionEnum { ret := i.getString(MaxTranscodeSize) @@ -1918,6 +1928,7 @@ func (i *Config) setDefaultValues() { i.setDefault(PreviewExcludeStart, previewExcludeStartDefault) i.setDefault(PreviewExcludeEnd, previewExcludeEndDefault) i.setDefault(PreviewAudio, previewAudioDefault) + i.setDefault(MaxMarkerPreviewDuration, maxMarkerPreviewDurationDefault) i.setDefault(SoundOnPreview, false) i.setDefault(UseCustomSpriteInterval, UseCustomSpriteIntervalDefault) diff --git a/internal/manager/task_generate_markers.go b/internal/manager/task_generate_markers.go index 1da458ba8..709a1015b 100644 --- a/internal/manager/task_generate_markers.go +++ b/internal/manager/task_generate_markers.go @@ -117,7 +117,7 @@ func (t *GenerateMarkersTask) generateMarker(videoFile *models.VideoFile, scene g := t.generator if t.VideoPreview { - if err := g.MarkerPreviewVideo(context.TODO(), videoFile.Path, sceneHash, seconds, sceneMarker.EndSeconds, instance.Config.GetPreviewAudio()); err != nil { + if err := g.MarkerPreviewVideo(context.TODO(), videoFile.Path, sceneHash, seconds, sceneMarker.EndSeconds, instance.Config.GetPreviewAudio(), instance.Config.GetMaxMarkerPreviewDuration()); err != nil { logger.Errorf("[generator] failed to generate marker video: %v", err) logErrorOutput(err) } diff --git a/pkg/scene/generate/marker_preview.go b/pkg/scene/generate/marker_preview.go index f14613591..83faec2fa 100644 --- a/pkg/scene/generate/marker_preview.go +++ b/pkg/scene/generate/marker_preview.go @@ -11,16 +11,20 @@ import ( const ( markerPreviewWidth = 640 - maxMarkerPreviewDuration = 20 markerPreviewAudioBitrate = "64k" + // Fallback duration for markers that lack a usable explicit end time + // (nil EndSeconds, or EndSeconds <= start). Not user-configurable; + // markers wanting a specific duration should set an end time. + markerPreviewDefaultDuration = 20 + markerImageDuration = 5 markerWebpFPS = 12 markerScreenshotQuality = 2 ) -func (g Generator) MarkerPreviewVideo(ctx context.Context, input string, hash string, seconds float64, endSeconds *float64, includeAudio bool) error { +func (g Generator) MarkerPreviewVideo(ctx context.Context, input string, hash string, seconds float64, endSeconds *float64, includeAudio bool, maxDuration int) error { lockCtx := g.LockManager.ReadLock(ctx, input) defer lockCtx.Cancel() @@ -31,11 +35,21 @@ func (g Generator) MarkerPreviewVideo(ctx context.Context, input string, hash st } } - duration := float64(maxMarkerPreviewDuration) + duration := float64(markerPreviewDefaultDuration) - // don't allow preview to exceed max duration - if endSeconds != nil && *endSeconds-seconds < maxMarkerPreviewDuration { - duration = float64(*endSeconds) - seconds + // Honor the marker's explicit interval when present and positive, capped + // by the configured safety ceiling. maxDuration <= 0 disables the ceiling. + if endSeconds != nil { + interval := *endSeconds - seconds + if interval > 0 { + if maxDuration <= 0 || interval <= float64(maxDuration) { + duration = interval + } else { + duration = float64(maxDuration) + } + } else { + logger.Warnf("[generator] marker at %.2fs has non-positive interval (end=%.2f); falling back to %ds default", seconds, *endSeconds, markerPreviewDefaultDuration) + } } if err := g.generateFile(lockCtx, g.MarkerPaths, mp4Pattern, output, g.markerPreviewVideo(input, sceneMarkerOptions{ diff --git a/ui/v2.5/graphql/data/config.graphql b/ui/v2.5/graphql/data/config.graphql index ba8215fe3..f940d6666 100644 --- a/ui/v2.5/graphql/data/config.graphql +++ b/ui/v2.5/graphql/data/config.graphql @@ -25,6 +25,7 @@ fragment ConfigGeneralData on ConfigGeneralResult { previewExcludeStart previewExcludeEnd previewPreset + maxMarkerPreviewDuration transcodeHardwareAcceleration maxTranscodeSize maxStreamingTranscodeSize diff --git a/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx b/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx index 446ad09a1..618ccd1cf 100644 --- a/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx @@ -425,6 +425,14 @@ export const SettingsConfigurationPanel: React.FC = () => { return <>; }} /> + + saveGeneral({ maxMarkerPreviewDuration: v })} + /> diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 4974c06ca..3cc754928 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -418,6 +418,8 @@ "include_audio_desc": "Includes audio stream when generating previews.", "include_audio_head": "Include audio", "logging": "Logging", + "max_marker_preview_duration_desc": "Ceiling (in seconds) applied to generated marker preview videos when the marker has an explicit end time. Protects against unexpectedly long previews from imports or data entry mistakes. Set to 0 to disable the ceiling and honor the marker's end time verbatim.", + "max_marker_preview_duration_head": "Max marker preview duration", "maximum_streaming_transcode_size_desc": "Maximum size for transcoded streams.", "maximum_streaming_transcode_size_head": "Maximum streaming transcode size", "maximum_transcode_size_desc": "Maximum size for generated transcodes.",