mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Allow customisation of preview generation (#673)
* Add generate-specific options * Include no-cache in preview response
This commit is contained in:
@@ -3,6 +3,10 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
|||||||
databasePath
|
databasePath
|
||||||
generatedPath
|
generatedPath
|
||||||
cachePath
|
cachePath
|
||||||
|
previewSegments
|
||||||
|
previewSegmentDuration
|
||||||
|
previewExcludeStart
|
||||||
|
previewExcludeEnd
|
||||||
previewPreset
|
previewPreset
|
||||||
maxTranscodeSize
|
maxTranscodeSize
|
||||||
maxStreamingTranscodeSize
|
maxStreamingTranscodeSize
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ input ConfigGeneralInput {
|
|||||||
generatedPath: String
|
generatedPath: String
|
||||||
"""Path to cache"""
|
"""Path to cache"""
|
||||||
cachePath: String
|
cachePath: String
|
||||||
|
"""Number of segments in a preview file"""
|
||||||
|
previewSegments: Int
|
||||||
|
"""Preview segment duration, in seconds"""
|
||||||
|
previewSegmentDuration: Float
|
||||||
|
"""Duration of start of video to exclude when generating previews"""
|
||||||
|
previewExcludeStart: String
|
||||||
|
"""Duration of end of video to exclude when generating previews"""
|
||||||
|
previewExcludeEnd: String
|
||||||
"""Preset when generating preview"""
|
"""Preset when generating preview"""
|
||||||
previewPreset: PreviewPreset
|
previewPreset: PreviewPreset
|
||||||
"""Max generated transcode size"""
|
"""Max generated transcode size"""
|
||||||
@@ -61,6 +69,14 @@ type ConfigGeneralResult {
|
|||||||
generatedPath: String!
|
generatedPath: String!
|
||||||
"""Path to cache"""
|
"""Path to cache"""
|
||||||
cachePath: String!
|
cachePath: String!
|
||||||
|
"""Number of segments in a preview file"""
|
||||||
|
previewSegments: Int!
|
||||||
|
"""Preview segment duration, in seconds"""
|
||||||
|
previewSegmentDuration: Float!
|
||||||
|
"""Duration of start of video to exclude when generating previews"""
|
||||||
|
previewExcludeStart: String!
|
||||||
|
"""Duration of end of video to exclude when generating previews"""
|
||||||
|
previewExcludeEnd: String!
|
||||||
"""Preset when generating preview"""
|
"""Preset when generating preview"""
|
||||||
previewPreset: PreviewPreset!
|
previewPreset: PreviewPreset!
|
||||||
"""Max generated transcode size"""
|
"""Max generated transcode size"""
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ input GenerateMetadataInput {
|
|||||||
sprites: Boolean!
|
sprites: Boolean!
|
||||||
previews: Boolean!
|
previews: Boolean!
|
||||||
imagePreviews: Boolean!
|
imagePreviews: Boolean!
|
||||||
|
previewOptions: GeneratePreviewOptionsInput
|
||||||
markers: Boolean!
|
markers: Boolean!
|
||||||
transcodes: Boolean!
|
transcodes: Boolean!
|
||||||
"""gallery thumbnails for cache usage"""
|
"""gallery thumbnails for cache usage"""
|
||||||
@@ -18,6 +19,19 @@ input GenerateMetadataInput {
|
|||||||
overwrite: Boolean
|
overwrite: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input GeneratePreviewOptionsInput {
|
||||||
|
"""Number of segments in a preview file"""
|
||||||
|
previewSegments: Int
|
||||||
|
"""Preview segment duration, in seconds"""
|
||||||
|
previewSegmentDuration: Float
|
||||||
|
"""Duration of start of video to exclude when generating previews"""
|
||||||
|
previewExcludeStart: String
|
||||||
|
"""Duration of end of video to exclude when generating previews"""
|
||||||
|
previewExcludeEnd: String
|
||||||
|
"""Preset when generating preview"""
|
||||||
|
previewPreset: PreviewPreset
|
||||||
|
}
|
||||||
|
|
||||||
input ScanMetadataInput {
|
input ScanMetadataInput {
|
||||||
useFileMetadata: Boolean!
|
useFileMetadata: Boolean!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,18 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
|||||||
config.Set(config.Cache, input.CachePath)
|
config.Set(config.Cache, input.CachePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if input.PreviewSegments != nil {
|
||||||
|
config.Set(config.PreviewSegments, *input.PreviewSegments)
|
||||||
|
}
|
||||||
|
if input.PreviewSegmentDuration != nil {
|
||||||
|
config.Set(config.PreviewSegmentDuration, *input.PreviewSegmentDuration)
|
||||||
|
}
|
||||||
|
if input.PreviewExcludeStart != nil {
|
||||||
|
config.Set(config.PreviewExcludeStart, *input.PreviewExcludeStart)
|
||||||
|
}
|
||||||
|
if input.PreviewExcludeEnd != nil {
|
||||||
|
config.Set(config.PreviewExcludeEnd, *input.PreviewExcludeEnd)
|
||||||
|
}
|
||||||
if input.PreviewPreset != nil {
|
if input.PreviewPreset != nil {
|
||||||
config.Set(config.PreviewPreset, input.PreviewPreset.String())
|
config.Set(config.PreviewPreset, input.PreviewPreset.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
|||||||
DatabasePath: config.GetDatabasePath(),
|
DatabasePath: config.GetDatabasePath(),
|
||||||
GeneratedPath: config.GetGeneratedPath(),
|
GeneratedPath: config.GetGeneratedPath(),
|
||||||
CachePath: config.GetCachePath(),
|
CachePath: config.GetCachePath(),
|
||||||
|
PreviewSegments: config.GetPreviewSegments(),
|
||||||
|
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
|
||||||
|
PreviewExcludeStart: config.GetPreviewExcludeStart(),
|
||||||
|
PreviewExcludeEnd: config.GetPreviewExcludeEnd(),
|
||||||
PreviewPreset: config.GetPreviewPreset(),
|
PreviewPreset: config.GetPreviewPreset(),
|
||||||
MaxTranscodeSize: &maxTranscodeSize,
|
MaxTranscodeSize: &maxTranscodeSize,
|
||||||
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
|
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ func (rs sceneRoutes) Screenshot(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (rs sceneRoutes) Preview(w http.ResponseWriter, r *http.Request) {
|
func (rs sceneRoutes) Preview(w http.ResponseWriter, r *http.Request) {
|
||||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||||
filepath := manager.GetInstance().Paths.Scene.GetStreamPreviewPath(scene.Checksum)
|
filepath := manager.GetInstance().Paths.Scene.GetStreamPreviewPath(scene.Checksum)
|
||||||
http.ServeFile(w, r, filepath)
|
utils.ServeFileNoCache(w, r, filepath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs sceneRoutes) Webp(w http.ResponseWriter, r *http.Request) {
|
func (rs sceneRoutes) Webp(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ScenePreviewChunkOptions struct {
|
type ScenePreviewChunkOptions struct {
|
||||||
Time int
|
StartTime float64
|
||||||
|
Duration float64
|
||||||
Width int
|
Width int
|
||||||
OutputPath string
|
OutputPath string
|
||||||
}
|
}
|
||||||
@@ -17,9 +18,9 @@ func (e *Encoder) ScenePreviewVideoChunk(probeResult VideoFile, options ScenePre
|
|||||||
args := []string{
|
args := []string{
|
||||||
"-v", "error",
|
"-v", "error",
|
||||||
"-xerror",
|
"-xerror",
|
||||||
"-ss", strconv.Itoa(options.Time),
|
"-ss", strconv.FormatFloat(options.StartTime, 'f', 2, 64),
|
||||||
"-i", probeResult.Path,
|
"-i", probeResult.Path,
|
||||||
"-t", "0.75",
|
"-t", strconv.FormatFloat(options.Duration, 'f', 2, 64),
|
||||||
"-max_muxing_queue_size", "1024", // https://trac.ffmpeg.org/ticket/6375
|
"-max_muxing_queue_size", "1024", // https://trac.ffmpeg.org/ticket/6375
|
||||||
"-y",
|
"-y",
|
||||||
"-c:v", "libx264",
|
"-c:v", "libx264",
|
||||||
|
|||||||
@@ -32,6 +32,18 @@ const PreviewPreset = "preview_preset"
|
|||||||
const MaxTranscodeSize = "max_transcode_size"
|
const MaxTranscodeSize = "max_transcode_size"
|
||||||
const MaxStreamingTranscodeSize = "max_streaming_transcode_size"
|
const MaxStreamingTranscodeSize = "max_streaming_transcode_size"
|
||||||
|
|
||||||
|
const PreviewSegmentDuration = "preview_segment_duration"
|
||||||
|
const previewSegmentDurationDefault = 0.75
|
||||||
|
|
||||||
|
const PreviewSegments = "preview_segments"
|
||||||
|
const previewSegmentsDefault = 12
|
||||||
|
|
||||||
|
const PreviewExcludeStart = "preview_exclude_start"
|
||||||
|
const previewExcludeStartDefault = "0"
|
||||||
|
|
||||||
|
const PreviewExcludeEnd = "preview_exclude_end"
|
||||||
|
const previewExcludeEndDefault = "0"
|
||||||
|
|
||||||
const Host = "host"
|
const Host = "host"
|
||||||
const Port = "port"
|
const Port = "port"
|
||||||
const ExternalHost = "external_host"
|
const ExternalHost = "external_host"
|
||||||
@@ -158,6 +170,36 @@ func GetExternalHost() string {
|
|||||||
return viper.GetString(ExternalHost)
|
return viper.GetString(ExternalHost)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPreviewSegmentDuration returns the duration of a single segment in a
|
||||||
|
// scene preview file, in seconds.
|
||||||
|
func GetPreviewSegmentDuration() float64 {
|
||||||
|
return viper.GetFloat64(PreviewSegmentDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPreviewSegments returns the amount of segments in a scene preview file.
|
||||||
|
func GetPreviewSegments() int {
|
||||||
|
return viper.GetInt(PreviewSegments)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPreviewExcludeStart returns the configuration setting string for
|
||||||
|
// excluding the start of scene videos for preview generation. This can
|
||||||
|
// be in two possible formats. A float value is interpreted as the amount
|
||||||
|
// of seconds to exclude from the start of the video before it is included
|
||||||
|
// in the preview. If the value is suffixed with a '%' character (for example
|
||||||
|
// '2%'), then it is interpreted as a proportion of the total video duration.
|
||||||
|
func GetPreviewExcludeStart() string {
|
||||||
|
return viper.GetString(PreviewExcludeStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPreviewExcludeEnd returns the configuration setting string for
|
||||||
|
// excluding the end of scene videos for preview generation. A float value
|
||||||
|
// is interpreted as the amount of seconds to exclude from the end of the video
|
||||||
|
// when generating previews. If the value is suffixed with a '%' character,
|
||||||
|
// then it is interpreted as a proportion of the total video duration.
|
||||||
|
func GetPreviewExcludeEnd() string {
|
||||||
|
return viper.GetString(PreviewExcludeEnd)
|
||||||
|
}
|
||||||
|
|
||||||
// GetPreviewPreset returns the preset when generating previews. Defaults to
|
// GetPreviewPreset returns the preset when generating previews. Defaults to
|
||||||
// Slow.
|
// Slow.
|
||||||
func GetPreviewPreset() models.PreviewPreset {
|
func GetPreviewPreset() models.PreviewPreset {
|
||||||
@@ -371,6 +413,13 @@ func IsValid() bool {
|
|||||||
return setPaths
|
return setPaths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setDefaultValues() {
|
||||||
|
viper.SetDefault(PreviewSegmentDuration, previewSegmentDurationDefault)
|
||||||
|
viper.SetDefault(PreviewSegments, previewSegmentsDefault)
|
||||||
|
viper.SetDefault(PreviewExcludeStart, previewExcludeStartDefault)
|
||||||
|
viper.SetDefault(PreviewExcludeEnd, previewExcludeEndDefault)
|
||||||
|
}
|
||||||
|
|
||||||
// SetInitialConfig fills in missing required config fields
|
// SetInitialConfig fills in missing required config fields
|
||||||
func SetInitialConfig() error {
|
func SetInitialConfig() error {
|
||||||
// generate some api keys
|
// generate some api keys
|
||||||
@@ -386,5 +435,7 @@ func SetInitialConfig() error {
|
|||||||
Set(SessionStoreKey, sessionStoreKey)
|
Set(SessionStoreKey, sessionStoreKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDefaultValues()
|
||||||
|
|
||||||
return Write()
|
return Write()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
@@ -17,7 +18,13 @@ type GeneratorInfo struct {
|
|||||||
ChunkCount int
|
ChunkCount int
|
||||||
FrameRate float64
|
FrameRate float64
|
||||||
NumberOfFrames int
|
NumberOfFrames int
|
||||||
NthFrame int
|
|
||||||
|
// NthFrame used for sprite generation
|
||||||
|
NthFrame int
|
||||||
|
|
||||||
|
ChunkDuration float64
|
||||||
|
ExcludeStart string
|
||||||
|
ExcludeEnd string
|
||||||
|
|
||||||
VideoFile ffmpeg.VideoFile
|
VideoFile ffmpeg.VideoFile
|
||||||
}
|
}
|
||||||
@@ -33,12 +40,7 @@ func newGeneratorInfo(videoFile ffmpeg.VideoFile) (*GeneratorInfo, error) {
|
|||||||
return generator, nil
|
return generator, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GeneratorInfo) configure() error {
|
func (g *GeneratorInfo) calculateFrameRate(videoStream *ffmpeg.FFProbeStream) error {
|
||||||
videoStream := g.VideoFile.VideoStream
|
|
||||||
if videoStream == nil {
|
|
||||||
return fmt.Errorf("missing video stream")
|
|
||||||
}
|
|
||||||
|
|
||||||
var framerate float64
|
var framerate float64
|
||||||
if g.VideoFile.FrameRate == 0 {
|
if g.VideoFile.FrameRate == 0 {
|
||||||
framerate, _ = strconv.ParseFloat(videoStream.RFrameRate, 64)
|
framerate, _ = strconv.ParseFloat(videoStream.RFrameRate, 64)
|
||||||
@@ -94,7 +96,54 @@ func (g *GeneratorInfo) configure() error {
|
|||||||
|
|
||||||
g.FrameRate = framerate
|
g.FrameRate = framerate
|
||||||
g.NumberOfFrames = numberOfFrames
|
g.NumberOfFrames = numberOfFrames
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GeneratorInfo) configure() error {
|
||||||
|
videoStream := g.VideoFile.VideoStream
|
||||||
|
if videoStream == nil {
|
||||||
|
return fmt.Errorf("missing video stream")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.calculateFrameRate(videoStream); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
g.NthFrame = g.NumberOfFrames / g.ChunkCount
|
g.NthFrame = g.NumberOfFrames / g.ChunkCount
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g GeneratorInfo) getExcludeValue(v string) float64 {
|
||||||
|
if strings.HasSuffix(v, "%") && len(v) > 1 {
|
||||||
|
// proportion of video duration
|
||||||
|
v = v[0 : len(v)-1]
|
||||||
|
prop, _ := strconv.ParseFloat(v, 64)
|
||||||
|
return prop / 100.0 * g.VideoFile.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
prop, _ := strconv.ParseFloat(v, 64)
|
||||||
|
return prop
|
||||||
|
}
|
||||||
|
|
||||||
|
// getStepSizeAndOffset calculates the step size for preview generation and
|
||||||
|
// the starting offset.
|
||||||
|
//
|
||||||
|
// Step size is calculated based on the duration of the video file, minus the
|
||||||
|
// excluded duration. The offset is based on the ExcludeStart. If the total
|
||||||
|
// excluded duration exceeds the duration of the video, then offset is 0, and
|
||||||
|
// the video duration is used to calculate the step size.
|
||||||
|
func (g GeneratorInfo) getStepSizeAndOffset() (stepSize float64, offset float64) {
|
||||||
|
duration := g.VideoFile.Duration
|
||||||
|
excludeStart := g.getExcludeValue(g.ExcludeStart)
|
||||||
|
excludeEnd := g.getExcludeValue(g.ExcludeEnd)
|
||||||
|
|
||||||
|
if duration > excludeStart+excludeEnd {
|
||||||
|
duration = duration - excludeStart - excludeEnd
|
||||||
|
offset = excludeStart
|
||||||
|
}
|
||||||
|
|
||||||
|
stepSize = duration / float64(g.ChunkCount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,9 +36,6 @@ func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, image
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
generator.ChunkCount = 12 // 12 segments to the preview
|
generator.ChunkCount = 12 // 12 segments to the preview
|
||||||
if err := generator.configure(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &PreviewGenerator{
|
return &PreviewGenerator{
|
||||||
Info: generator,
|
Info: generator,
|
||||||
@@ -53,6 +50,11 @@ func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, image
|
|||||||
|
|
||||||
func (g *PreviewGenerator) Generate() error {
|
func (g *PreviewGenerator) Generate() error {
|
||||||
logger.Infof("[generator] generating scene preview for %s", g.Info.VideoFile.Path)
|
logger.Infof("[generator] generating scene preview for %s", g.Info.VideoFile.Path)
|
||||||
|
|
||||||
|
if err := g.Info.configure(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
encoder := ffmpeg.NewEncoder(instance.FFMPEGPath)
|
encoder := ffmpeg.NewEncoder(instance.FFMPEGPath)
|
||||||
|
|
||||||
if err := g.generateConcatFile(); err != nil {
|
if err := g.generateConcatFile(); err != nil {
|
||||||
@@ -95,15 +97,17 @@ func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
stepSize := int(g.Info.VideoFile.Duration / float64(g.Info.ChunkCount))
|
stepSize, offset := g.Info.getStepSizeAndOffset()
|
||||||
|
|
||||||
for i := 0; i < g.Info.ChunkCount; i++ {
|
for i := 0; i < g.Info.ChunkCount; i++ {
|
||||||
time := i * stepSize
|
time := offset + (float64(i) * stepSize)
|
||||||
num := fmt.Sprintf("%.3d", i)
|
num := fmt.Sprintf("%.3d", i)
|
||||||
filename := "preview" + num + ".mp4"
|
filename := "preview" + num + ".mp4"
|
||||||
chunkOutputPath := instance.Paths.Generated.GetTmpPath(filename)
|
chunkOutputPath := instance.Paths.Generated.GetTmpPath(filename)
|
||||||
|
|
||||||
options := ffmpeg.ScenePreviewChunkOptions{
|
options := ffmpeg.ScenePreviewChunkOptions{
|
||||||
Time: time,
|
StartTime: time,
|
||||||
|
Duration: g.Info.ChunkDuration,
|
||||||
Width: 640,
|
Width: 640,
|
||||||
OutputPath: chunkOutputPath,
|
OutputPath: chunkOutputPath,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ func initConfig() {
|
|||||||
}
|
}
|
||||||
logger.Infof("using config file: %s", viper.ConfigFileUsed())
|
logger.Infof("using config file: %s", viper.ConfigFileUsed())
|
||||||
|
|
||||||
|
config.SetInitialConfig()
|
||||||
|
|
||||||
viper.SetDefault(config.Database, paths.GetDefaultDatabaseFilePath())
|
viper.SetDefault(config.Database, paths.GetDefaultDatabaseFilePath())
|
||||||
|
|
||||||
// Set generated to the metadata path for backwards compat
|
// Set generated to the metadata path for backwards compat
|
||||||
|
|||||||
@@ -167,6 +167,33 @@ func (s *singleton) Export() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setGeneratePreviewOptionsInput(optionsInput *models.GeneratePreviewOptionsInput) {
|
||||||
|
if optionsInput.PreviewSegments == nil {
|
||||||
|
val := config.GetPreviewSegments()
|
||||||
|
optionsInput.PreviewSegments = &val
|
||||||
|
}
|
||||||
|
|
||||||
|
if optionsInput.PreviewSegmentDuration == nil {
|
||||||
|
val := config.GetPreviewSegmentDuration()
|
||||||
|
optionsInput.PreviewSegmentDuration = &val
|
||||||
|
}
|
||||||
|
|
||||||
|
if optionsInput.PreviewExcludeStart == nil {
|
||||||
|
val := config.GetPreviewExcludeStart()
|
||||||
|
optionsInput.PreviewExcludeStart = &val
|
||||||
|
}
|
||||||
|
|
||||||
|
if optionsInput.PreviewExcludeEnd == nil {
|
||||||
|
val := config.GetPreviewExcludeEnd()
|
||||||
|
optionsInput.PreviewExcludeEnd = &val
|
||||||
|
}
|
||||||
|
|
||||||
|
if optionsInput.PreviewPreset == nil {
|
||||||
|
val := config.GetPreviewPreset()
|
||||||
|
optionsInput.PreviewPreset = &val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *singleton) Generate(input models.GenerateMetadataInput) {
|
func (s *singleton) Generate(input models.GenerateMetadataInput) {
|
||||||
if s.Status.Status != Idle {
|
if s.Status.Status != Idle {
|
||||||
return
|
return
|
||||||
@@ -181,8 +208,6 @@ func (s *singleton) Generate(input models.GenerateMetadataInput) {
|
|||||||
//this.job.total = await ObjectionUtils.getCount(Scene);
|
//this.job.total = await ObjectionUtils.getCount(Scene);
|
||||||
instance.Paths.Generated.EnsureTmpDir()
|
instance.Paths.Generated.EnsureTmpDir()
|
||||||
|
|
||||||
preset := config.GetPreviewPreset().String()
|
|
||||||
|
|
||||||
galleryIDs := utils.StringSliceToIntSlice(input.GalleryIDs)
|
galleryIDs := utils.StringSliceToIntSlice(input.GalleryIDs)
|
||||||
sceneIDs := utils.StringSliceToIntSlice(input.SceneIDs)
|
sceneIDs := utils.StringSliceToIntSlice(input.SceneIDs)
|
||||||
markerIDs := utils.StringSliceToIntSlice(input.MarkerIDs)
|
markerIDs := utils.StringSliceToIntSlice(input.MarkerIDs)
|
||||||
@@ -251,6 +276,12 @@ func (s *singleton) Generate(input models.GenerateMetadataInput) {
|
|||||||
overwrite = *input.Overwrite
|
overwrite = *input.Overwrite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generatePreviewOptions := input.PreviewOptions
|
||||||
|
if generatePreviewOptions == nil {
|
||||||
|
generatePreviewOptions = &models.GeneratePreviewOptionsInput{}
|
||||||
|
}
|
||||||
|
setGeneratePreviewOptionsInput(generatePreviewOptions)
|
||||||
|
|
||||||
for i, scene := range scenes {
|
for i, scene := range scenes {
|
||||||
s.Status.setProgress(i, total)
|
s.Status.setProgress(i, total)
|
||||||
if s.Status.stopping {
|
if s.Status.stopping {
|
||||||
@@ -276,7 +307,12 @@ func (s *singleton) Generate(input models.GenerateMetadataInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if input.Previews {
|
if input.Previews {
|
||||||
task := GeneratePreviewTask{Scene: *scene, ImagePreview: input.ImagePreviews, PreviewPreset: preset, Overwrite: overwrite}
|
task := GeneratePreviewTask{
|
||||||
|
Scene: *scene,
|
||||||
|
ImagePreview: input.ImagePreviews,
|
||||||
|
Options: *generatePreviewOptions,
|
||||||
|
Overwrite: overwrite,
|
||||||
|
}
|
||||||
go task.Start(&wg)
|
go task.Start(&wg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type GeneratePreviewTask struct {
|
type GeneratePreviewTask struct {
|
||||||
Scene models.Scene
|
Scene models.Scene
|
||||||
ImagePreview bool
|
ImagePreview bool
|
||||||
PreviewPreset string
|
|
||||||
Overwrite bool
|
Options models.GeneratePreviewOptionsInput
|
||||||
|
|
||||||
|
Overwrite bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *GeneratePreviewTask) Start(wg *sync.WaitGroup) {
|
func (t *GeneratePreviewTask) Start(wg *sync.WaitGroup) {
|
||||||
@@ -32,13 +34,19 @@ func (t *GeneratePreviewTask) Start(wg *sync.WaitGroup) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
generator, err := NewPreviewGenerator(*videoFile, videoFilename, imageFilename, instance.Paths.Generated.Screenshots, true, t.ImagePreview, t.PreviewPreset)
|
generator, err := NewPreviewGenerator(*videoFile, videoFilename, imageFilename, instance.Paths.Generated.Screenshots, true, t.ImagePreview, t.Options.PreviewPreset.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("error creating preview generator: %s", err.Error())
|
logger.Errorf("error creating preview generator: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
generator.Overwrite = t.Overwrite
|
generator.Overwrite = t.Overwrite
|
||||||
|
|
||||||
|
// set the preview generation configuration from the global config
|
||||||
|
generator.Info.ChunkCount = *t.Options.PreviewSegments
|
||||||
|
generator.Info.ChunkDuration = *t.Options.PreviewSegmentDuration
|
||||||
|
generator.Info.ExcludeStart = *t.Options.PreviewExcludeStart
|
||||||
|
generator.Info.ExcludeEnd = *t.Options.PreviewExcludeEnd
|
||||||
|
|
||||||
if err := generator.Generate(); err != nil {
|
if err := generator.Generate(); err != nil {
|
||||||
logger.Errorf("error generating preview: %s", err.Error())
|
logger.Errorf("error generating preview: %s", err.Error())
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/h2non/filetype"
|
|
||||||
"github.com/h2non/filetype/types"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math"
|
"math"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/h2non/filetype"
|
||||||
|
"github.com/h2non/filetype/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileType uses the filetype package to determine the given file path's type
|
// FileType uses the filetype package to determine the given file path's type
|
||||||
@@ -219,3 +221,11 @@ func GetParent(path string) *string {
|
|||||||
return &parentPath
|
return &parentPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServeFileNoCache serves the provided file, ensuring that the response
|
||||||
|
// contains headers to prevent caching.
|
||||||
|
func ServeFileNoCache(w http.ResponseWriter, r *http.Request, filepath string) {
|
||||||
|
w.Header().Add("Cache-Control", "no-cache")
|
||||||
|
|
||||||
|
http.ServeFile(w, r, filepath)
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const markup = `
|
|||||||
* Add support for parent/child studios.
|
* Add support for parent/child studios.
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Allow customisation of preview video generation.
|
||||||
* Add support for live transcoding in Safari.
|
* Add support for live transcoding in Safari.
|
||||||
* Add mapped and fixed post-processing scraping options.
|
* Add mapped and fixed post-processing scraping options.
|
||||||
* Add random sorting for performers.
|
* Add random sorting for performers.
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Form } from "react-bootstrap";
|
import { Form, Button, Collapse } from "react-bootstrap";
|
||||||
import { mutateMetadataGenerate } from "src/core/StashService";
|
import {
|
||||||
import { Modal } from "src/components/Shared";
|
mutateMetadataGenerate,
|
||||||
|
useConfiguration,
|
||||||
|
} from "src/core/StashService";
|
||||||
|
import { Modal, Icon } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
|
||||||
interface ISceneGenerateDialogProps {
|
interface ISceneGenerateDialogProps {
|
||||||
selectedIds: string[];
|
selectedIds: string[];
|
||||||
@@ -12,6 +16,8 @@ interface ISceneGenerateDialogProps {
|
|||||||
export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||||
props: ISceneGenerateDialogProps
|
props: ISceneGenerateDialogProps
|
||||||
) => {
|
) => {
|
||||||
|
const { data, error, loading } = useConfiguration();
|
||||||
|
|
||||||
const [sprites, setSprites] = useState(true);
|
const [sprites, setSprites] = useState(true);
|
||||||
const [previews, setPreviews] = useState(true);
|
const [previews, setPreviews] = useState(true);
|
||||||
const [markers, setMarkers] = useState(true);
|
const [markers, setMarkers] = useState(true);
|
||||||
@@ -19,8 +25,37 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
const [overwrite, setOverwrite] = useState(true);
|
const [overwrite, setOverwrite] = useState(true);
|
||||||
const [imagePreviews, setImagePreviews] = useState(false);
|
const [imagePreviews, setImagePreviews] = useState(false);
|
||||||
|
|
||||||
|
const [previewSegments, setPreviewSegments] = useState<number>(0);
|
||||||
|
const [previewSegmentDuration, setPreviewSegmentDuration] = useState<number>(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const [previewExcludeStart, setPreviewExcludeStart] = useState<
|
||||||
|
string | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [previewExcludeEnd, setPreviewExcludeEnd] = useState<
|
||||||
|
string | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [previewPreset, setPreviewPreset] = useState<string>(
|
||||||
|
GQL.PreviewPreset.Slow
|
||||||
|
);
|
||||||
|
|
||||||
|
const [previewOptionsOpen, setPreviewOptionsOpen] = useState(false);
|
||||||
|
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data?.configuration) return;
|
||||||
|
|
||||||
|
const conf = data.configuration;
|
||||||
|
if (conf.general) {
|
||||||
|
setPreviewSegments(conf.general.previewSegments);
|
||||||
|
setPreviewSegmentDuration(conf.general.previewSegmentDuration);
|
||||||
|
setPreviewExcludeStart(conf.general.previewExcludeStart);
|
||||||
|
setPreviewExcludeEnd(conf.general.previewExcludeEnd);
|
||||||
|
setPreviewPreset(conf.general.previewPreset);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
async function onGenerate() {
|
async function onGenerate() {
|
||||||
try {
|
try {
|
||||||
await mutateMetadataGenerate({
|
await mutateMetadataGenerate({
|
||||||
@@ -32,6 +67,13 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
thumbnails: false,
|
thumbnails: false,
|
||||||
overwrite,
|
overwrite,
|
||||||
sceneIDs: props.selectedIds,
|
sceneIDs: props.selectedIds,
|
||||||
|
previewOptions: {
|
||||||
|
previewPreset: (previewPreset as GQL.PreviewPreset) ?? undefined,
|
||||||
|
previewSegments,
|
||||||
|
previewSegmentDuration,
|
||||||
|
previewExcludeStart,
|
||||||
|
previewExcludeEnd,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
Toast.success({ content: "Started generating" });
|
Toast.success({ content: "Started generating" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -41,6 +83,15 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
Toast.error(error);
|
||||||
|
props.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
show
|
show
|
||||||
@@ -72,6 +123,109 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
className="ml-2 flex-grow"
|
className="ml-2 flex-grow"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="my-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => setPreviewOptionsOpen(!previewOptionsOpen)}
|
||||||
|
className="minimal pl-0 no-focus"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={previewOptionsOpen ? "chevron-down" : "chevron-right"}
|
||||||
|
/>
|
||||||
|
<span>Preview Options</span>
|
||||||
|
</Button>
|
||||||
|
<Collapse in={previewOptionsOpen}>
|
||||||
|
<div>
|
||||||
|
<Form.Group id="transcode-size">
|
||||||
|
<h6>Preview encoding preset</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="w-auto input-control"
|
||||||
|
as="select"
|
||||||
|
value={previewPreset}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
setPreviewPreset(e.currentTarget.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{Object.keys(GQL.PreviewPreset).map((p) => (
|
||||||
|
<option value={p.toLowerCase()} key={p}>
|
||||||
|
{p}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Form.Control>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
The preset regulates size, quality and encoding time of
|
||||||
|
preview generation. Presets beyond “slow” have diminishing
|
||||||
|
returns and are not recommended.
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group id="preview-segments">
|
||||||
|
<h6>Number of segments in preview</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="col col-sm-6 text-input"
|
||||||
|
type="number"
|
||||||
|
value={previewSegments.toString()}
|
||||||
|
onInput={(e: React.FormEvent<HTMLInputElement>) =>
|
||||||
|
setPreviewSegments(
|
||||||
|
Number.parseInt(e.currentTarget.value, 10)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
Number of segments in preview files.
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group id="preview-segment-duration">
|
||||||
|
<h6>Preview segment duration</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="col col-sm-6 text-input"
|
||||||
|
type="number"
|
||||||
|
value={previewSegmentDuration.toString()}
|
||||||
|
onInput={(e: React.FormEvent<HTMLInputElement>) =>
|
||||||
|
setPreviewSegmentDuration(
|
||||||
|
Number.parseFloat(e.currentTarget.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
Duration of each preview segment, in seconds.
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group id="preview-exclude-start">
|
||||||
|
<h6>Exclude start time</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="col col-sm-6 text-input"
|
||||||
|
defaultValue={previewExcludeStart}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPreviewExcludeStart(e.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
Exclude the first x seconds from scene previews. This can be
|
||||||
|
a value in seconds, or a percentage (eg 2%) of the total
|
||||||
|
scene duration.
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group id="preview-exclude-start">
|
||||||
|
<h6>Exclude end time</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="col col-sm-6 text-input"
|
||||||
|
defaultValue={previewExcludeEnd}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPreviewExcludeEnd(e.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
Exclude the last x seconds from scene previews. This can be
|
||||||
|
a value in seconds, or a percentage (eg 2%) of the total
|
||||||
|
scene duration.
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="sprite-task"
|
id="sprite-task"
|
||||||
checked={sprites}
|
checked={sprites}
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
const [cachePath, setCachePath] = useState<string | undefined>(undefined);
|
const [cachePath, setCachePath] = useState<string | undefined>(undefined);
|
||||||
|
const [previewSegments, setPreviewSegments] = useState<number>(0);
|
||||||
|
const [previewSegmentDuration, setPreviewSegmentDuration] = useState<number>(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const [previewExcludeStart, setPreviewExcludeStart] = useState<
|
||||||
|
string | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [previewExcludeEnd, setPreviewExcludeEnd] = useState<
|
||||||
|
string | undefined
|
||||||
|
>(undefined);
|
||||||
const [previewPreset, setPreviewPreset] = useState<string>(
|
const [previewPreset, setPreviewPreset] = useState<string>(
|
||||||
GQL.PreviewPreset.Slow
|
GQL.PreviewPreset.Slow
|
||||||
);
|
);
|
||||||
@@ -45,6 +55,10 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
databasePath,
|
databasePath,
|
||||||
generatedPath,
|
generatedPath,
|
||||||
cachePath,
|
cachePath,
|
||||||
|
previewSegments,
|
||||||
|
previewSegmentDuration,
|
||||||
|
previewExcludeStart,
|
||||||
|
previewExcludeEnd,
|
||||||
previewPreset: (previewPreset as GQL.PreviewPreset) ?? undefined,
|
previewPreset: (previewPreset as GQL.PreviewPreset) ?? undefined,
|
||||||
maxTranscodeSize,
|
maxTranscodeSize,
|
||||||
maxStreamingTranscodeSize,
|
maxStreamingTranscodeSize,
|
||||||
@@ -68,6 +82,10 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
setDatabasePath(conf.general.databasePath);
|
setDatabasePath(conf.general.databasePath);
|
||||||
setGeneratedPath(conf.general.generatedPath);
|
setGeneratedPath(conf.general.generatedPath);
|
||||||
setCachePath(conf.general.cachePath);
|
setCachePath(conf.general.cachePath);
|
||||||
|
setPreviewSegments(conf.general.previewSegments);
|
||||||
|
setPreviewSegmentDuration(conf.general.previewSegmentDuration);
|
||||||
|
setPreviewExcludeStart(conf.general.previewExcludeStart);
|
||||||
|
setPreviewExcludeEnd(conf.general.previewExcludeEnd);
|
||||||
setPreviewPreset(conf.general.previewPreset);
|
setPreviewPreset(conf.general.previewPreset);
|
||||||
setMaxTranscodeSize(conf.general.maxTranscodeSize ?? undefined);
|
setMaxTranscodeSize(conf.general.maxTranscodeSize ?? undefined);
|
||||||
setMaxStreamingTranscodeSize(
|
setMaxStreamingTranscodeSize(
|
||||||
@@ -273,28 +291,6 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h4>Video</h4>
|
<h4>Video</h4>
|
||||||
<Form.Group id="transcode-size">
|
|
||||||
<h6>Preview encoding preset</h6>
|
|
||||||
<Form.Control
|
|
||||||
className="w-auto input-control"
|
|
||||||
as="select"
|
|
||||||
value={previewPreset}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
|
||||||
setPreviewPreset(e.currentTarget.value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{Object.keys(GQL.PreviewPreset).map((p) => (
|
|
||||||
<option value={p.toLowerCase()} key={p}>
|
|
||||||
{p}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Form.Control>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
The preset regulates size, quality and encoding time of preview
|
|
||||||
generation. Presets beyond “slow” have diminishing returns and are
|
|
||||||
not recommended.
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group id="transcode-size">
|
<Form.Group id="transcode-size">
|
||||||
<h6>Maximum transcode size</h6>
|
<h6>Maximum transcode size</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -341,6 +337,94 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
|
<Form.Group>
|
||||||
|
<h4>Preview Generation</h4>
|
||||||
|
|
||||||
|
<Form.Group id="transcode-size">
|
||||||
|
<h6>Preview encoding preset</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="w-auto input-control"
|
||||||
|
as="select"
|
||||||
|
value={previewPreset}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
setPreviewPreset(e.currentTarget.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{Object.keys(GQL.PreviewPreset).map((p) => (
|
||||||
|
<option value={p.toLowerCase()} key={p}>
|
||||||
|
{p}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Form.Control>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
The preset regulates size, quality and encoding time of preview
|
||||||
|
generation. Presets beyond “slow” have diminishing returns and are
|
||||||
|
not recommended.
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group id="preview-segments">
|
||||||
|
<h6>Number of segments in preview</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="col col-sm-6 text-input"
|
||||||
|
type="number"
|
||||||
|
value={previewSegments.toString()}
|
||||||
|
onInput={(e: React.FormEvent<HTMLInputElement>) =>
|
||||||
|
setPreviewSegments(Number.parseInt(e.currentTarget.value, 10))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
Number of segments in preview files.
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group id="preview-segment-duration">
|
||||||
|
<h6>Preview segment duration</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="col col-sm-6 text-input"
|
||||||
|
type="number"
|
||||||
|
value={previewSegmentDuration.toString()}
|
||||||
|
onInput={(e: React.FormEvent<HTMLInputElement>) =>
|
||||||
|
setPreviewSegmentDuration(
|
||||||
|
Number.parseFloat(e.currentTarget.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
Duration of each preview segment, in seconds.
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group id="preview-exclude-start">
|
||||||
|
<h6>Exclude start time</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="col col-sm-6 text-input"
|
||||||
|
defaultValue={previewExcludeStart}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPreviewExcludeStart(e.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
Exclude the first x seconds from scene previews. This can be a value
|
||||||
|
in seconds, or a percentage (eg 2%) of the total scene duration.
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group id="preview-exclude-start">
|
||||||
|
<h6>Exclude end time</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="col col-sm-6 text-input"
|
||||||
|
defaultValue={previewExcludeEnd}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPreviewExcludeEnd(e.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
Exclude the last x seconds from scene previews. This can be a value
|
||||||
|
in seconds, or a percentage (eg 2%) of the total scene duration.
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="generated-path">
|
<Form.Group id="generated-path">
|
||||||
<h6>Scraping</h6>
|
<h6>Scraping</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
|||||||
@@ -577,3 +577,9 @@ div.dropdown-menu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-focus:focus {
|
||||||
|
background-color: inherit;
|
||||||
|
border-color: inherit;
|
||||||
|
box-shadow: inherit;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user