mirror of
https://github.com/stashapp/stash.git
synced 2025-12-15 12:52:38 +01:00
Allow customisation of preview generation (#673)
* Add generate-specific options * Include no-cache in preview response
This commit is contained in:
parent
37be146a9d
commit
a2341f0819
18 changed files with 509 additions and 53 deletions
|
|
@ -3,6 +3,10 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
|||
databasePath
|
||||
generatedPath
|
||||
cachePath
|
||||
previewSegments
|
||||
previewSegmentDuration
|
||||
previewExcludeStart
|
||||
previewExcludeEnd
|
||||
previewPreset
|
||||
maxTranscodeSize
|
||||
maxStreamingTranscodeSize
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ input ConfigGeneralInput {
|
|||
generatedPath: String
|
||||
"""Path to cache"""
|
||||
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"""
|
||||
previewPreset: PreviewPreset
|
||||
"""Max generated transcode size"""
|
||||
|
|
@ -61,6 +69,14 @@ type ConfigGeneralResult {
|
|||
generatedPath: String!
|
||||
"""Path to cache"""
|
||||
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"""
|
||||
previewPreset: PreviewPreset!
|
||||
"""Max generated transcode size"""
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ input GenerateMetadataInput {
|
|||
sprites: Boolean!
|
||||
previews: Boolean!
|
||||
imagePreviews: Boolean!
|
||||
previewOptions: GeneratePreviewOptionsInput
|
||||
markers: Boolean!
|
||||
transcodes: Boolean!
|
||||
"""gallery thumbnails for cache usage"""
|
||||
|
|
@ -18,6 +19,19 @@ input GenerateMetadataInput {
|
|||
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 {
|
||||
useFileMetadata: Boolean!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,18 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
|||
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 {
|
||||
config.Set(config.PreviewPreset, input.PreviewPreset.String())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
|||
DatabasePath: config.GetDatabasePath(),
|
||||
GeneratedPath: config.GetGeneratedPath(),
|
||||
CachePath: config.GetCachePath(),
|
||||
PreviewSegments: config.GetPreviewSegments(),
|
||||
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
|
||||
PreviewExcludeStart: config.GetPreviewExcludeStart(),
|
||||
PreviewExcludeEnd: config.GetPreviewExcludeEnd(),
|
||||
PreviewPreset: config.GetPreviewPreset(),
|
||||
MaxTranscodeSize: &maxTranscodeSize,
|
||||
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) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import (
|
|||
)
|
||||
|
||||
type ScenePreviewChunkOptions struct {
|
||||
Time int
|
||||
StartTime float64
|
||||
Duration float64
|
||||
Width int
|
||||
OutputPath string
|
||||
}
|
||||
|
|
@ -17,9 +18,9 @@ func (e *Encoder) ScenePreviewVideoChunk(probeResult VideoFile, options ScenePre
|
|||
args := []string{
|
||||
"-v", "error",
|
||||
"-xerror",
|
||||
"-ss", strconv.Itoa(options.Time),
|
||||
"-ss", strconv.FormatFloat(options.StartTime, 'f', 2, 64),
|
||||
"-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
|
||||
"-y",
|
||||
"-c:v", "libx264",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,18 @@ const PreviewPreset = "preview_preset"
|
|||
const MaxTranscodeSize = "max_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 Port = "port"
|
||||
const ExternalHost = "external_host"
|
||||
|
|
@ -158,6 +170,36 @@ func GetExternalHost() string {
|
|||
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
|
||||
// Slow.
|
||||
func GetPreviewPreset() models.PreviewPreset {
|
||||
|
|
@ -371,6 +413,13 @@ func IsValid() bool {
|
|||
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
|
||||
func SetInitialConfig() error {
|
||||
// generate some api keys
|
||||
|
|
@ -386,5 +435,7 @@ func SetInitialConfig() error {
|
|||
Set(SessionStoreKey, sessionStoreKey)
|
||||
}
|
||||
|
||||
setDefaultValues()
|
||||
|
||||
return Write()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
|
|
@ -17,7 +18,13 @@ type GeneratorInfo struct {
|
|||
ChunkCount int
|
||||
FrameRate float64
|
||||
NumberOfFrames int
|
||||
NthFrame int
|
||||
|
||||
// NthFrame used for sprite generation
|
||||
NthFrame int
|
||||
|
||||
ChunkDuration float64
|
||||
ExcludeStart string
|
||||
ExcludeEnd string
|
||||
|
||||
VideoFile ffmpeg.VideoFile
|
||||
}
|
||||
|
|
@ -33,12 +40,7 @@ func newGeneratorInfo(videoFile ffmpeg.VideoFile) (*GeneratorInfo, error) {
|
|||
return generator, nil
|
||||
}
|
||||
|
||||
func (g *GeneratorInfo) configure() error {
|
||||
videoStream := g.VideoFile.VideoStream
|
||||
if videoStream == nil {
|
||||
return fmt.Errorf("missing video stream")
|
||||
}
|
||||
|
||||
func (g *GeneratorInfo) calculateFrameRate(videoStream *ffmpeg.FFProbeStream) error {
|
||||
var framerate float64
|
||||
if g.VideoFile.FrameRate == 0 {
|
||||
framerate, _ = strconv.ParseFloat(videoStream.RFrameRate, 64)
|
||||
|
|
@ -94,7 +96,54 @@ func (g *GeneratorInfo) configure() error {
|
|||
|
||||
g.FrameRate = framerate
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
generator.ChunkCount = 12 // 12 segments to the preview
|
||||
if err := generator.configure(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PreviewGenerator{
|
||||
Info: generator,
|
||||
|
|
@ -53,6 +50,11 @@ func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, image
|
|||
|
||||
func (g *PreviewGenerator) Generate() error {
|
||||
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)
|
||||
|
||||
if err := g.generateConcatFile(); err != nil {
|
||||
|
|
@ -95,15 +97,17 @@ func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error {
|
|||
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++ {
|
||||
time := i * stepSize
|
||||
time := offset + (float64(i) * stepSize)
|
||||
num := fmt.Sprintf("%.3d", i)
|
||||
filename := "preview" + num + ".mp4"
|
||||
chunkOutputPath := instance.Paths.Generated.GetTmpPath(filename)
|
||||
|
||||
options := ffmpeg.ScenePreviewChunkOptions{
|
||||
Time: time,
|
||||
StartTime: time,
|
||||
Duration: g.Info.ChunkDuration,
|
||||
Width: 640,
|
||||
OutputPath: chunkOutputPath,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ func initConfig() {
|
|||
}
|
||||
logger.Infof("using config file: %s", viper.ConfigFileUsed())
|
||||
|
||||
config.SetInitialConfig()
|
||||
|
||||
viper.SetDefault(config.Database, paths.GetDefaultDatabaseFilePath())
|
||||
|
||||
// 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) {
|
||||
if s.Status.Status != Idle {
|
||||
return
|
||||
|
|
@ -181,8 +208,6 @@ func (s *singleton) Generate(input models.GenerateMetadataInput) {
|
|||
//this.job.total = await ObjectionUtils.getCount(Scene);
|
||||
instance.Paths.Generated.EnsureTmpDir()
|
||||
|
||||
preset := config.GetPreviewPreset().String()
|
||||
|
||||
galleryIDs := utils.StringSliceToIntSlice(input.GalleryIDs)
|
||||
sceneIDs := utils.StringSliceToIntSlice(input.SceneIDs)
|
||||
markerIDs := utils.StringSliceToIntSlice(input.MarkerIDs)
|
||||
|
|
@ -251,6 +276,12 @@ func (s *singleton) Generate(input models.GenerateMetadataInput) {
|
|||
overwrite = *input.Overwrite
|
||||
}
|
||||
|
||||
generatePreviewOptions := input.PreviewOptions
|
||||
if generatePreviewOptions == nil {
|
||||
generatePreviewOptions = &models.GeneratePreviewOptionsInput{}
|
||||
}
|
||||
setGeneratePreviewOptionsInput(generatePreviewOptions)
|
||||
|
||||
for i, scene := range scenes {
|
||||
s.Status.setProgress(i, total)
|
||||
if s.Status.stopping {
|
||||
|
|
@ -276,7 +307,12 @@ func (s *singleton) Generate(input models.GenerateMetadataInput) {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ import (
|
|||
)
|
||||
|
||||
type GeneratePreviewTask struct {
|
||||
Scene models.Scene
|
||||
ImagePreview bool
|
||||
PreviewPreset string
|
||||
Overwrite bool
|
||||
Scene models.Scene
|
||||
ImagePreview bool
|
||||
|
||||
Options models.GeneratePreviewOptionsInput
|
||||
|
||||
Overwrite bool
|
||||
}
|
||||
|
||||
func (t *GeneratePreviewTask) Start(wg *sync.WaitGroup) {
|
||||
|
|
@ -32,13 +34,19 @@ func (t *GeneratePreviewTask) Start(wg *sync.WaitGroup) {
|
|||
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 {
|
||||
logger.Errorf("error creating preview generator: %s", err.Error())
|
||||
return
|
||||
}
|
||||
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 {
|
||||
logger.Errorf("error generating preview: %s", err.Error())
|
||||
return
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@ package utils
|
|||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"github.com/h2non/filetype"
|
||||
"github.com/h2non/filetype/types"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/h2non/filetype"
|
||||
"github.com/h2non/filetype/types"
|
||||
)
|
||||
|
||||
// FileType uses the filetype package to determine the given file path's type
|
||||
|
|
@ -219,3 +221,11 @@ func GetParent(path string) *string {
|
|||
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.
|
||||
|
||||
### 🎨 Improvements
|
||||
* Allow customisation of preview video generation.
|
||||
* Add support for live transcoding in Safari.
|
||||
* Add mapped and fixed post-processing scraping options.
|
||||
* Add random sorting for performers.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import React, { useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { mutateMetadataGenerate } from "src/core/StashService";
|
||||
import { Modal } from "src/components/Shared";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Form, Button, Collapse } from "react-bootstrap";
|
||||
import {
|
||||
mutateMetadataGenerate,
|
||||
useConfiguration,
|
||||
} from "src/core/StashService";
|
||||
import { Modal, Icon } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
|
||||
interface ISceneGenerateDialogProps {
|
||||
selectedIds: string[];
|
||||
|
|
@ -12,6 +16,8 @@ interface ISceneGenerateDialogProps {
|
|||
export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
props: ISceneGenerateDialogProps
|
||||
) => {
|
||||
const { data, error, loading } = useConfiguration();
|
||||
|
||||
const [sprites, setSprites] = useState(true);
|
||||
const [previews, setPreviews] = useState(true);
|
||||
const [markers, setMarkers] = useState(true);
|
||||
|
|
@ -19,8 +25,37 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||
const [overwrite, setOverwrite] = useState(true);
|
||||
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();
|
||||
|
||||
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() {
|
||||
try {
|
||||
await mutateMetadataGenerate({
|
||||
|
|
@ -32,6 +67,13 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||
thumbnails: false,
|
||||
overwrite,
|
||||
sceneIDs: props.selectedIds,
|
||||
previewOptions: {
|
||||
previewPreset: (previewPreset as GQL.PreviewPreset) ?? undefined,
|
||||
previewSegments,
|
||||
previewSegmentDuration,
|
||||
previewExcludeStart,
|
||||
previewExcludeEnd,
|
||||
},
|
||||
});
|
||||
Toast.success({ content: "Started generating" });
|
||||
} catch (e) {
|
||||
|
|
@ -41,6 +83,15 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
Toast.error(error);
|
||||
props.onClose();
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show
|
||||
|
|
@ -72,6 +123,109 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||
className="ml-2 flex-grow"
|
||||
/>
|
||||
</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
|
||||
id="sprite-task"
|
||||
checked={sprites}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||
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>(
|
||||
GQL.PreviewPreset.Slow
|
||||
);
|
||||
|
|
@ -45,6 +55,10 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||
databasePath,
|
||||
generatedPath,
|
||||
cachePath,
|
||||
previewSegments,
|
||||
previewSegmentDuration,
|
||||
previewExcludeStart,
|
||||
previewExcludeEnd,
|
||||
previewPreset: (previewPreset as GQL.PreviewPreset) ?? undefined,
|
||||
maxTranscodeSize,
|
||||
maxStreamingTranscodeSize,
|
||||
|
|
@ -68,6 +82,10 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||
setDatabasePath(conf.general.databasePath);
|
||||
setGeneratedPath(conf.general.generatedPath);
|
||||
setCachePath(conf.general.cachePath);
|
||||
setPreviewSegments(conf.general.previewSegments);
|
||||
setPreviewSegmentDuration(conf.general.previewSegmentDuration);
|
||||
setPreviewExcludeStart(conf.general.previewExcludeStart);
|
||||
setPreviewExcludeEnd(conf.general.previewExcludeEnd);
|
||||
setPreviewPreset(conf.general.previewPreset);
|
||||
setMaxTranscodeSize(conf.general.maxTranscodeSize ?? undefined);
|
||||
setMaxStreamingTranscodeSize(
|
||||
|
|
@ -273,28 +291,6 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||
|
||||
<Form.Group>
|
||||
<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">
|
||||
<h6>Maximum transcode size</h6>
|
||||
<Form.Control
|
||||
|
|
@ -341,6 +337,94 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||
|
||||
<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">
|
||||
<h6>Scraping</h6>
|
||||
<Form.Control
|
||||
|
|
|
|||
|
|
@ -577,3 +577,9 @@ div.dropdown-menu {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-focus:focus {
|
||||
background-color: inherit;
|
||||
border-color: inherit;
|
||||
box-shadow: inherit;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue