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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 509 additions and 53 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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