Add option to generate image thumbnails during generate (#4602)

* Add option to generate image thumbnails
* Limit number of concurrent image thumbnail generation ops
This commit is contained in:
WithoutPants 2024-02-22 11:19:23 +11:00 committed by GitHub
parent c4a91d15a6
commit a8c909e0c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 332 additions and 222 deletions

View file

@ -12,6 +12,7 @@ input GenerateMetadataInput {
forceTranscodes: Boolean forceTranscodes: Boolean
phashes: Boolean phashes: Boolean
interactiveHeatmapsSpeeds: Boolean interactiveHeatmapsSpeeds: Boolean
imageThumbnails: Boolean
clipPreviews: Boolean clipPreviews: Boolean
"scene ids to generate for" "scene ids to generate for"
@ -48,6 +49,7 @@ type GenerateMetadataOptions {
transcodes: Boolean transcodes: Boolean
phashes: Boolean phashes: Boolean
interactiveHeatmapsSpeeds: Boolean interactiveHeatmapsSpeeds: Boolean
imageThumbnails: Boolean
clipPreviews: Boolean clipPreviews: Boolean
} }

View file

@ -46,8 +46,9 @@ func (rs imageRoutes) Routes() chi.Router {
} }
func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) { func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
mgr := manager.GetInstance()
img := r.Context().Value(imageKey).(*models.Image) img := r.Context().Value(imageKey).(*models.Image)
filepath := manager.GetInstance().Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth) filepath := mgr.Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth)
// if the thumbnail doesn't exist, encode on the fly // if the thumbnail doesn't exist, encode on the fly
exists, _ := fsutil.FileExists(filepath) exists, _ := fsutil.FileExists(filepath)
@ -62,6 +63,11 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
return return
} }
// use the image thumbnail generate wait group to limit the number of concurrent thumbnail generation tasks
wg := &mgr.ImageThumbnailGenerateWaitGroup
wg.Add()
defer wg.Done()
clipPreviewOptions := image.ClipPreviewOptions{ clipPreviewOptions := image.ClipPreviewOptions{
InputArgs: manager.GetInstance().Config.GetTranscodeInputArgs(), InputArgs: manager.GetInstance().Config.GetTranscodeInputArgs(),
OutputArgs: manager.GetInstance().Config.GetTranscodeOutputArgs(), OutputArgs: manager.GetInstance().Config.GetTranscodeOutputArgs(),

View file

@ -9,6 +9,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/remeh/sizedwaitgroup"
"github.com/stashapp/stash/internal/desktop" "github.com/stashapp/stash/internal/desktop"
"github.com/stashapp/stash/internal/dlna" "github.com/stashapp/stash/internal/dlna"
"github.com/stashapp/stash/internal/log" "github.com/stashapp/stash/internal/log"
@ -80,6 +81,8 @@ func Initialize(cfg *config.Config, l *log.Logger) (*Manager, error) {
Paths: mgrPaths, Paths: mgrPaths,
ImageThumbnailGenerateWaitGroup: sizedwaitgroup.New(1),
JobManager: initJobManager(cfg), JobManager: initJobManager(cfg),
ReadLockManager: fsutil.NewReadLockManager(), ReadLockManager: fsutil.NewReadLockManager(),

View file

@ -10,6 +10,7 @@ import (
"runtime" "runtime"
"time" "time"
"github.com/remeh/sizedwaitgroup"
"github.com/stashapp/stash/internal/dlna" "github.com/stashapp/stash/internal/dlna"
"github.com/stashapp/stash/internal/log" "github.com/stashapp/stash/internal/log"
"github.com/stashapp/stash/internal/manager/config" "github.com/stashapp/stash/internal/manager/config"
@ -33,6 +34,10 @@ type Manager struct {
Config *config.Config Config *config.Config
Logger *log.Logger Logger *log.Logger
// ImageThumbnailGenerateWaitGroup is the global wait group image thumbnail generation
// It uses the parallel tasks setting from the configuration.
ImageThumbnailGenerateWaitGroup sizedwaitgroup.SizedWaitGroup
Paths *paths.Paths Paths *paths.Paths
FFMpeg *ffmpeg.FFMpeg FFMpeg *ffmpeg.FFMpeg
@ -107,6 +112,8 @@ func (s *Manager) RefreshConfig() {
if err := fsutil.EnsureDir(s.Paths.Generated.InteractiveHeatmap); err != nil { if err := fsutil.EnsureDir(s.Paths.Generated.InteractiveHeatmap); err != nil {
logger.Warnf("could not create interactive heatmaps directory: %v", err) logger.Warnf("could not create interactive heatmaps directory: %v", err)
} }
s.ImageThumbnailGenerateWaitGroup.Size = cfg.GetParallelTasksWithAutoDetection()
} }
} }

View file

@ -31,6 +31,7 @@ type GenerateMetadataInput struct {
Phashes bool `json:"phashes"` Phashes bool `json:"phashes"`
InteractiveHeatmapsSpeeds bool `json:"interactiveHeatmapsSpeeds"` InteractiveHeatmapsSpeeds bool `json:"interactiveHeatmapsSpeeds"`
ClipPreviews bool `json:"clipPreviews"` ClipPreviews bool `json:"clipPreviews"`
ImageThumbnails bool `json:"imageThumbnails"`
// scene ids to generate for // scene ids to generate for
SceneIDs []string `json:"sceneIDs"` SceneIDs []string `json:"sceneIDs"`
// marker ids to generate for // marker ids to generate for
@ -60,6 +61,8 @@ type GenerateJob struct {
overwrite bool overwrite bool
fileNamingAlgo models.HashAlgorithm fileNamingAlgo models.HashAlgorithm
totals totalsGenerate
} }
type totalsGenerate struct { type totalsGenerate struct {
@ -72,6 +75,7 @@ type totalsGenerate struct {
phashes int64 phashes int64
interactiveHeatmapSpeeds int64 interactiveHeatmapSpeeds int64
clipPreviews int64 clipPreviews int64
imageThumbnails int64
tasks int tasks int
} }
@ -93,7 +97,6 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
go func() { go func() {
defer close(queue) defer close(queue)
var totals totalsGenerate
sceneIDs, err := stringslice.StringSliceToIntSlice(j.input.SceneIDs) sceneIDs, err := stringslice.StringSliceToIntSlice(j.input.SceneIDs)
if err != nil { if err != nil {
logger.Error(err.Error()) logger.Error(err.Error())
@ -116,7 +119,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
if err := r.WithReadTxn(ctx, func(ctx context.Context) error { if err := r.WithReadTxn(ctx, func(ctx context.Context) error {
qb := r.Scene qb := r.Scene
if len(j.input.SceneIDs) == 0 && len(j.input.MarkerIDs) == 0 { if len(j.input.SceneIDs) == 0 && len(j.input.MarkerIDs) == 0 {
totals = j.queueTasks(ctx, g, queue) j.queueTasks(ctx, g, queue)
} else { } else {
if len(j.input.SceneIDs) > 0 { if len(j.input.SceneIDs) > 0 {
scenes, err = qb.FindMany(ctx, sceneIDs) scenes, err = qb.FindMany(ctx, sceneIDs)
@ -125,7 +128,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
return err return err
} }
j.queueSceneJobs(ctx, g, s, queue, &totals) j.queueSceneJobs(ctx, g, s, queue)
} }
} }
@ -135,7 +138,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
return err return err
} }
for _, m := range markers { for _, m := range markers {
j.queueMarkerJob(g, m, queue, &totals) j.queueMarkerJob(g, m, queue)
} }
} }
} }
@ -146,6 +149,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
return return
} }
totals := j.totals
logMsg := "Generating" logMsg := "Generating"
if j.input.Covers { if j.input.Covers {
logMsg += fmt.Sprintf(" %d covers", totals.covers) logMsg += fmt.Sprintf(" %d covers", totals.covers)
@ -174,6 +178,9 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
if j.input.ClipPreviews { if j.input.ClipPreviews {
logMsg += fmt.Sprintf(" %d Image Clip Previews", totals.clipPreviews) logMsg += fmt.Sprintf(" %d Image Clip Previews", totals.clipPreviews)
} }
if j.input.ImageThumbnails {
logMsg += fmt.Sprintf(" %d Image Thumbnails", totals.imageThumbnails)
}
if logMsg == "Generating" { if logMsg == "Generating" {
logMsg = "Nothing selected to generate" logMsg = "Nothing selected to generate"
} }
@ -223,9 +230,14 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
logger.Info(fmt.Sprintf("Generate finished (%s)", elapsed)) logger.Info(fmt.Sprintf("Generate finished (%s)", elapsed))
} }
func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) totalsGenerate { func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) {
var totals totalsGenerate j.totals = totalsGenerate{}
j.queueScenesTasks(ctx, g, queue)
j.queueImagesTasks(ctx, g, queue)
}
func (j *GenerateJob) queueScenesTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) {
const batchSize = 1000 const batchSize = 1000
findFilter := models.BatchFindFilter(batchSize) findFilter := models.BatchFindFilter(batchSize)
@ -234,26 +246,26 @@ func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, que
for more := true; more; { for more := true; more; {
if job.IsCancelled(ctx) { if job.IsCancelled(ctx) {
return totals return
} }
scenes, err := scene.Query(ctx, r.Scene, nil, findFilter) scenes, err := scene.Query(ctx, r.Scene, nil, findFilter)
if err != nil { if err != nil {
logger.Errorf("Error encountered queuing files to scan: %s", err.Error()) logger.Errorf("Error encountered queuing files to scan: %s", err.Error())
return totals return
} }
for _, ss := range scenes { for _, ss := range scenes {
if job.IsCancelled(ctx) { if job.IsCancelled(ctx) {
return totals return
} }
if err := ss.LoadFiles(ctx, r.Scene); err != nil { if err := ss.LoadFiles(ctx, r.Scene); err != nil {
logger.Errorf("Error encountered queuing files to scan: %s", err.Error()) logger.Errorf("Error encountered queuing files to scan: %s", err.Error())
return totals return
} }
j.queueSceneJobs(ctx, g, ss, queue, &totals) j.queueSceneJobs(ctx, g, ss, queue)
} }
if len(scenes) != batchSize { if len(scenes) != batchSize {
@ -262,30 +274,37 @@ func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, que
*findFilter.Page++ *findFilter.Page++
} }
} }
}
*findFilter.Page = 1 func (j *GenerateJob) queueImagesTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) {
for more := j.input.ClipPreviews; more; { const batchSize = 1000
findFilter := models.BatchFindFilter(batchSize)
r := j.repository
for more := j.input.ClipPreviews || j.input.ImageThumbnails; more; {
if job.IsCancelled(ctx) { if job.IsCancelled(ctx) {
return totals return
} }
images, err := image.Query(ctx, r.Image, nil, findFilter) images, err := image.Query(ctx, r.Image, nil, findFilter)
if err != nil { if err != nil {
logger.Errorf("Error encountered queuing files to scan: %s", err.Error()) logger.Errorf("Error encountered queuing files to scan: %s", err.Error())
return totals return
} }
for _, ss := range images { for _, ss := range images {
if job.IsCancelled(ctx) { if job.IsCancelled(ctx) {
return totals return
} }
if err := ss.LoadFiles(ctx, r.Image); err != nil { if err := ss.LoadFiles(ctx, r.Image); err != nil {
logger.Errorf("Error encountered queuing files to scan: %s", err.Error()) logger.Errorf("Error encountered queuing files to scan: %s", err.Error())
return totals return
} }
j.queueImageJob(g, ss, queue, &totals) j.queueImageJob(g, ss, queue)
} }
if len(images) != batchSize { if len(images) != batchSize {
@ -294,8 +313,6 @@ func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, que
*findFilter.Page++ *findFilter.Page++
} }
} }
return totals
} }
func getGeneratePreviewOptions(optionsInput GeneratePreviewOptionsInput) generate.PreviewOptions { func getGeneratePreviewOptions(optionsInput GeneratePreviewOptionsInput) generate.PreviewOptions {
@ -333,7 +350,7 @@ func getGeneratePreviewOptions(optionsInput GeneratePreviewOptionsInput) generat
return ret return ret
} }
func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator, scene *models.Scene, queue chan<- Task, totals *totalsGenerate) { func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator, scene *models.Scene, queue chan<- Task) {
r := j.repository r := j.repository
if j.input.Covers { if j.input.Covers {
@ -344,8 +361,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
} }
if task.required(ctx) { if task.required(ctx) {
totals.covers++ j.totals.covers++
totals.tasks++ j.totals.tasks++
queue <- task queue <- task
} }
} }
@ -358,8 +375,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
} }
if task.required() { if task.required() {
totals.sprites++ j.totals.sprites++
totals.tasks++ j.totals.tasks++
queue <- task queue <- task
} }
} }
@ -382,13 +399,13 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
if task.required() { if task.required() {
if task.videoPreviewRequired() { if task.videoPreviewRequired() {
totals.previews++ j.totals.previews++
} }
if task.imagePreviewRequired() { if task.imagePreviewRequired() {
totals.imagePreviews++ j.totals.imagePreviews++
} }
totals.tasks++ j.totals.tasks++
queue <- task queue <- task
} }
} }
@ -407,8 +424,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
markers := task.markersNeeded(ctx) markers := task.markersNeeded(ctx)
if markers > 0 { if markers > 0 {
totals.markers += int64(markers) j.totals.markers += int64(markers)
totals.tasks++ j.totals.tasks++
queue <- task queue <- task
} }
@ -424,8 +441,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
g: g, g: g,
} }
if task.required() { if task.required() {
totals.transcodes++ j.totals.transcodes++
totals.tasks++ j.totals.tasks++
queue <- task queue <- task
} }
} }
@ -441,8 +458,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
} }
if task.required() { if task.required() {
totals.phashes++ j.totals.phashes++
totals.tasks++ j.totals.tasks++
queue <- task queue <- task
} }
} }
@ -457,14 +474,14 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
} }
if task.required() { if task.required() {
totals.interactiveHeatmapSpeeds++ j.totals.interactiveHeatmapSpeeds++
totals.tasks++ j.totals.tasks++
queue <- task queue <- task
} }
} }
} }
func (j *GenerateJob) queueMarkerJob(g *generate.Generator, marker *models.SceneMarker, queue chan<- Task, totals *totalsGenerate) { func (j *GenerateJob) queueMarkerJob(g *generate.Generator, marker *models.SceneMarker, queue chan<- Task) {
task := &GenerateMarkersTask{ task := &GenerateMarkersTask{
repository: j.repository, repository: j.repository,
Marker: marker, Marker: marker,
@ -472,20 +489,35 @@ func (j *GenerateJob) queueMarkerJob(g *generate.Generator, marker *models.Scene
fileNamingAlgorithm: j.fileNamingAlgo, fileNamingAlgorithm: j.fileNamingAlgo,
generator: g, generator: g,
} }
totals.markers++ j.totals.markers++
totals.tasks++ j.totals.tasks++
queue <- task queue <- task
} }
func (j *GenerateJob) queueImageJob(g *generate.Generator, image *models.Image, queue chan<- Task, totals *totalsGenerate) { func (j *GenerateJob) queueImageJob(g *generate.Generator, image *models.Image, queue chan<- Task) {
if j.input.ImageThumbnails {
task := &GenerateImageThumbnailTask{
Image: *image,
Overwrite: j.overwrite,
}
if task.required() {
j.totals.imageThumbnails++
j.totals.tasks++
queue <- task
}
}
if j.input.ClipPreviews {
task := &GenerateClipPreviewTask{ task := &GenerateClipPreviewTask{
Image: *image, Image: *image,
Overwrite: j.overwrite, Overwrite: j.overwrite,
} }
if task.required() { if task.required() {
totals.clipPreviews++ j.totals.clipPreviews++
totals.tasks++ j.totals.tasks++
queue <- task queue <- task
} }
}
} }

View file

@ -0,0 +1,79 @@
package manager
import (
"context"
"errors"
"fmt"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
)
type GenerateImageThumbnailTask struct {
Image models.Image
Overwrite bool
}
func (t *GenerateImageThumbnailTask) GetDescription() string {
return fmt.Sprintf("Generating Thumbnail for image %s", t.Image.Path)
}
func (t *GenerateImageThumbnailTask) Start(ctx context.Context) {
if !t.required() {
return
}
thumbPath := GetInstance().Paths.Generated.GetThumbnailPath(t.Image.Checksum, models.DefaultGthumbWidth)
f := t.Image.Files.Primary()
path := f.Base().Path
logger.Debugf("Generating thumbnail for %s", path)
mgr := GetInstance()
c := mgr.Config
clipPreviewOptions := image.ClipPreviewOptions{
InputArgs: c.GetTranscodeInputArgs(),
OutputArgs: c.GetTranscodeOutputArgs(),
Preset: c.GetPreviewPreset().String(),
}
encoder := image.NewThumbnailEncoder(mgr.FFMpeg, mgr.FFProbe, clipPreviewOptions)
data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth)
if err != nil {
// don't log for animated images
if !errors.Is(err, image.ErrNotSupportedForThumbnail) {
logger.Errorf("[generator] getting thumbnail for image %s: %w", path, err)
}
return
}
err = fsutil.WriteFile(thumbPath, data)
if err != nil {
logger.Errorf("[generator] writing thumbnail for image %s: %w", path, err)
return
}
}
func (t *GenerateImageThumbnailTask) required() bool {
vf, ok := t.Image.Files.Primary().(models.VisualFile)
if !ok {
return false
}
if vf.GetHeight() <= models.DefaultGthumbWidth && vf.GetWidth() <= models.DefaultGthumbWidth {
return false
}
if t.Overwrite {
return true
}
thumbPath := GetInstance().Paths.Generated.GetThumbnailPath(t.Image.Checksum, models.DefaultGthumbWidth)
exists, _ := fsutil.FileExists(thumbPath)
return !exists
}

View file

@ -2,7 +2,6 @@ package manager
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io/fs" "io/fs"
"path/filepath" "path/filepath"
@ -412,9 +411,12 @@ func (g *imageGenerators) Generate(ctx context.Context, i *models.Image, f model
if t.ScanGenerateThumbnails { if t.ScanGenerateThumbnails {
// this should be quick, so always generate sequentially // this should be quick, so always generate sequentially
if err := g.generateThumbnail(ctx, i, f); err != nil { taskThumbnail := GenerateImageThumbnailTask{
logger.Errorf("Error generating thumbnail for %s: %v", path, err) Image: *i,
Overwrite: overwrite,
} }
taskThumbnail.Start(ctx)
} }
// avoid adding a task if the file isn't a video file // avoid adding a task if the file isn't a video file
@ -446,54 +448,6 @@ func (g *imageGenerators) Generate(ctx context.Context, i *models.Image, f model
return nil return nil
} }
func (g *imageGenerators) generateThumbnail(ctx context.Context, i *models.Image, f models.File) error {
thumbPath := g.paths.Generated.GetThumbnailPath(i.Checksum, models.DefaultGthumbWidth)
exists, _ := fsutil.FileExists(thumbPath)
if exists {
return nil
}
path := f.Base().Path
vf, ok := f.(models.VisualFile)
if !ok {
return fmt.Errorf("file %s is not a visual file", path)
}
if vf.GetHeight() <= models.DefaultGthumbWidth && vf.GetWidth() <= models.DefaultGthumbWidth {
return nil
}
logger.Debugf("Generating thumbnail for %s", path)
mgr := GetInstance()
c := mgr.Config
clipPreviewOptions := image.ClipPreviewOptions{
InputArgs: c.GetTranscodeInputArgs(),
OutputArgs: c.GetTranscodeOutputArgs(),
Preset: c.GetPreviewPreset().String(),
}
encoder := image.NewThumbnailEncoder(mgr.FFMpeg, mgr.FFProbe, clipPreviewOptions)
data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth)
if err != nil {
// don't log for animated images
if !errors.Is(err, image.ErrNotSupportedForThumbnail) {
return fmt.Errorf("getting thumbnail for image %s: %w", path, err)
}
return nil
}
err = fsutil.WriteFile(thumbPath, data)
if err != nil {
return fmt.Errorf("writing thumbnail for image %s: %w", path, err)
}
return nil
}
type sceneGenerators struct { type sceneGenerators struct {
input ScanMetadataInput input ScanMetadataInput
taskQueue *job.TaskQueue taskQueue *job.TaskQueue

View file

@ -18,6 +18,7 @@ type GenerateMetadataOptions struct {
Transcodes bool `json:"transcodes"` Transcodes bool `json:"transcodes"`
Phashes bool `json:"phashes"` Phashes bool `json:"phashes"`
InteractiveHeatmapsSpeeds bool `json:"interactiveHeatmapsSpeeds"` InteractiveHeatmapsSpeeds bool `json:"interactiveHeatmapsSpeeds"`
ImageThumbnails bool `json:"imageThumbnails"`
ClipPreviews bool `json:"clipPreviews"` ClipPreviews bool `json:"clipPreviews"`
} }

View file

@ -196,6 +196,7 @@ fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult {
phashes phashes
interactiveHeatmapsSpeeds interactiveHeatmapsSpeeds
clipPreviews clipPreviews
imageThumbnails
} }
deleteFile deleteFile

View file

@ -17,11 +17,13 @@ import { SettingsContext } from "../Settings/context";
interface ISceneGenerateDialog { interface ISceneGenerateDialog {
selectedIds?: string[]; selectedIds?: string[];
onClose: () => void; onClose: () => void;
type: "scene"; // TODO - add image generate
} }
export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
selectedIds, selectedIds,
onClose, onClose,
type,
}) => { }) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = React.useContext(ConfigurationContext);
@ -200,6 +202,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
<SettingsContext> <SettingsContext>
<SettingSection> <SettingSection>
<GenerateOptions <GenerateOptions
type={type}
options={options} options={options}
setOptions={setOptions} setOptions={setOptions}
selection selection

View file

@ -278,6 +278,7 @@ const ScenePage: React.FC<IProps> = ({
onClose={() => { onClose={() => {
setIsGenerateDialogOpen(false); setIsGenerateDialogOpen(false);
}} }}
type="scene"
/> />
); );
} }

View file

@ -235,6 +235,7 @@ export const SceneList: React.FC<ISceneList> = ({
if (isGenerateDialogOpen) { if (isGenerateDialogOpen) {
return ( return (
<GenerateDialog <GenerateDialog
type="scene"
selectedIds={Array.from(selectedIds.values())} selectedIds={Array.from(selectedIds.values())}
onClose={() => setIsGenerateDialogOpen(false)} onClose={() => setIsGenerateDialogOpen(false)}
/> />

View file

@ -7,12 +7,14 @@ import {
} from "../GeneratePreviewOptions"; } from "../GeneratePreviewOptions";
interface IGenerateOptions { interface IGenerateOptions {
type?: "scene" | "image";
selection?: boolean; selection?: boolean;
options: GQL.GenerateMetadataInput; options: GQL.GenerateMetadataInput;
setOptions: (s: GQL.GenerateMetadataInput) => void; setOptions: (s: GQL.GenerateMetadataInput) => void;
} }
export const GenerateOptions: React.FC<IGenerateOptions> = ({ export const GenerateOptions: React.FC<IGenerateOptions> = ({
type,
selection, selection,
options, options,
setOptions: setOptionsState, setOptions: setOptionsState,
@ -24,7 +26,12 @@ export const GenerateOptions: React.FC<IGenerateOptions> = ({
setOptionsState({ ...options, ...input }); setOptionsState({ ...options, ...input });
} }
const showSceneOptions = !type || type === "scene";
const showImageOptions = !type || type === "image";
return ( return (
<>
{showSceneOptions && (
<> <>
<BooleanSetting <BooleanSetting
id="covers-task" id="covers-task"
@ -148,12 +155,24 @@ export const GenerateOptions: React.FC<IGenerateOptions> = ({
headingID="dialogs.scene_gen.interactive_heatmap_speed" headingID="dialogs.scene_gen.interactive_heatmap_speed"
onChange={(v) => setOptions({ interactiveHeatmapsSpeeds: v })} onChange={(v) => setOptions({ interactiveHeatmapsSpeeds: v })}
/> />
</>
)}
{showImageOptions && (
<>
<BooleanSetting <BooleanSetting
id="clip-previews" id="clip-previews"
checked={options.clipPreviews ?? false} checked={options.clipPreviews ?? false}
headingID="dialogs.scene_gen.clip_previews" headingID="dialogs.scene_gen.clip_previews"
onChange={(v) => setOptions({ clipPreviews: v })} onChange={(v) => setOptions({ clipPreviews: v })}
/> />
<BooleanSetting
id="image-thumbnails"
checked={options.imageThumbnails ?? false}
headingID="dialogs.scene_gen.image_thumbnails"
onChange={(v) => setOptions({ imageThumbnails: v })}
/>
</>
)}
<BooleanSetting <BooleanSetting
id="overwrite" id="overwrite"
checked={options.overwrite ?? false} checked={options.overwrite ?? false}

View file

@ -875,6 +875,7 @@
"force_transcodes_tooltip": "By default, transcodes are only generated when the video file is not supported in the browser. When enabled, transcodes will be generated even when the video file appears to be supported in the browser.", "force_transcodes_tooltip": "By default, transcodes are only generated when the video file is not supported in the browser. When enabled, transcodes will be generated even when the video file appears to be supported in the browser.",
"image_previews": "Animated Image Previews", "image_previews": "Animated Image Previews",
"image_previews_tooltip": "Also generate animated (webp) previews, only required when Scene/Marker Wall Preview Type is set to Animated Image. When browsing they use less CPU than the video previews, but are generated in addition to them and are larger files.", "image_previews_tooltip": "Also generate animated (webp) previews, only required when Scene/Marker Wall Preview Type is set to Animated Image. When browsing they use less CPU than the video previews, but are generated in addition to them and are larger files.",
"image_thumbnails": "Image Thumbnails",
"interactive_heatmap_speed": "Generate heatmaps and speeds for interactive scenes", "interactive_heatmap_speed": "Generate heatmaps and speeds for interactive scenes",
"marker_image_previews": "Marker Animated Image Previews", "marker_image_previews": "Marker Animated Image Previews",
"marker_image_previews_tooltip": "Also generate animated (webp) previews, only required when Scene/Marker Wall Preview Type is set to Animated Image. When browsing they use less CPU than the video previews, but are generated in addition to them and are larger files.", "marker_image_previews_tooltip": "Also generate animated (webp) previews, only required when Scene/Marker Wall Preview Type is set to Animated Image. When browsing they use less CPU than the video previews, but are generated in addition to them and are larger files.",