Allow customisation of preview generation (#673)

* Add generate-specific options
* Include no-cache in preview response
This commit is contained in:
WithoutPants
2020-07-23 12:51:35 +10:00
committed by GitHub
parent 37be146a9d
commit a2341f0819
18 changed files with 509 additions and 53 deletions

View File

@@ -3,6 +3,10 @@ fragment ConfigGeneralData on ConfigGeneralResult {
databasePath databasePath
generatedPath generatedPath
cachePath cachePath
previewSegments
previewSegmentDuration
previewExcludeStart
previewExcludeEnd
previewPreset previewPreset
maxTranscodeSize maxTranscodeSize
maxStreamingTranscodeSize maxStreamingTranscodeSize

View File

@@ -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"""

View File

@@ -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!
} }

View File

@@ -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())
} }

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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()
} }

View File

@@ -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
}

View File

@@ -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,
} }

View File

@@ -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

View File

@@ -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)
} }

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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.

View File

@@ -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}

View File

@@ -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

View File

@@ -577,3 +577,9 @@ div.dropdown-menu {
} }
} }
} }
.no-focus:focus {
background-color: inherit;
border-color: inherit;
box-shadow: inherit;
}