mirror of https://github.com/stashapp/stash.git
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:
parent
c4a91d15a6
commit
a8c909e0c9
|
@ -12,6 +12,7 @@ input GenerateMetadataInput {
|
|||
forceTranscodes: Boolean
|
||||
phashes: Boolean
|
||||
interactiveHeatmapsSpeeds: Boolean
|
||||
imageThumbnails: Boolean
|
||||
clipPreviews: Boolean
|
||||
|
||||
"scene ids to generate for"
|
||||
|
@ -48,6 +49,7 @@ type GenerateMetadataOptions {
|
|||
transcodes: Boolean
|
||||
phashes: Boolean
|
||||
interactiveHeatmapsSpeeds: Boolean
|
||||
imageThumbnails: Boolean
|
||||
clipPreviews: Boolean
|
||||
}
|
||||
|
||||
|
|
|
@ -46,8 +46,9 @@ func (rs imageRoutes) Routes() chi.Router {
|
|||
}
|
||||
|
||||
func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
mgr := manager.GetInstance()
|
||||
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
|
||||
exists, _ := fsutil.FileExists(filepath)
|
||||
|
@ -62,6 +63,11 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
|||
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{
|
||||
InputArgs: manager.GetInstance().Config.GetTranscodeInputArgs(),
|
||||
OutputArgs: manager.GetInstance().Config.GetTranscodeOutputArgs(),
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/remeh/sizedwaitgroup"
|
||||
"github.com/stashapp/stash/internal/desktop"
|
||||
"github.com/stashapp/stash/internal/dlna"
|
||||
"github.com/stashapp/stash/internal/log"
|
||||
|
@ -80,6 +81,8 @@ func Initialize(cfg *config.Config, l *log.Logger) (*Manager, error) {
|
|||
|
||||
Paths: mgrPaths,
|
||||
|
||||
ImageThumbnailGenerateWaitGroup: sizedwaitgroup.New(1),
|
||||
|
||||
JobManager: initJobManager(cfg),
|
||||
ReadLockManager: fsutil.NewReadLockManager(),
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/remeh/sizedwaitgroup"
|
||||
"github.com/stashapp/stash/internal/dlna"
|
||||
"github.com/stashapp/stash/internal/log"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
|
@ -33,6 +34,10 @@ type Manager struct {
|
|||
Config *config.Config
|
||||
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
|
||||
|
||||
FFMpeg *ffmpeg.FFMpeg
|
||||
|
@ -107,6 +112,8 @@ func (s *Manager) RefreshConfig() {
|
|||
if err := fsutil.EnsureDir(s.Paths.Generated.InteractiveHeatmap); err != nil {
|
||||
logger.Warnf("could not create interactive heatmaps directory: %v", err)
|
||||
}
|
||||
|
||||
s.ImageThumbnailGenerateWaitGroup.Size = cfg.GetParallelTasksWithAutoDetection()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ type GenerateMetadataInput struct {
|
|||
Phashes bool `json:"phashes"`
|
||||
InteractiveHeatmapsSpeeds bool `json:"interactiveHeatmapsSpeeds"`
|
||||
ClipPreviews bool `json:"clipPreviews"`
|
||||
ImageThumbnails bool `json:"imageThumbnails"`
|
||||
// scene ids to generate for
|
||||
SceneIDs []string `json:"sceneIDs"`
|
||||
// marker ids to generate for
|
||||
|
@ -60,6 +61,8 @@ type GenerateJob struct {
|
|||
|
||||
overwrite bool
|
||||
fileNamingAlgo models.HashAlgorithm
|
||||
|
||||
totals totalsGenerate
|
||||
}
|
||||
|
||||
type totalsGenerate struct {
|
||||
|
@ -72,6 +75,7 @@ type totalsGenerate struct {
|
|||
phashes int64
|
||||
interactiveHeatmapSpeeds int64
|
||||
clipPreviews int64
|
||||
imageThumbnails int64
|
||||
|
||||
tasks int
|
||||
}
|
||||
|
@ -93,7 +97,6 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
|
|||
go func() {
|
||||
defer close(queue)
|
||||
|
||||
var totals totalsGenerate
|
||||
sceneIDs, err := stringslice.StringSliceToIntSlice(j.input.SceneIDs)
|
||||
if err != nil {
|
||||
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 {
|
||||
qb := r.Scene
|
||||
if len(j.input.SceneIDs) == 0 && len(j.input.MarkerIDs) == 0 {
|
||||
totals = j.queueTasks(ctx, g, queue)
|
||||
j.queueTasks(ctx, g, queue)
|
||||
} else {
|
||||
if len(j.input.SceneIDs) > 0 {
|
||||
scenes, err = qb.FindMany(ctx, sceneIDs)
|
||||
|
@ -125,7 +128,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
|
|||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
totals := j.totals
|
||||
logMsg := "Generating"
|
||||
if j.input.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 {
|
||||
logMsg += fmt.Sprintf(" %d Image Clip Previews", totals.clipPreviews)
|
||||
}
|
||||
if j.input.ImageThumbnails {
|
||||
logMsg += fmt.Sprintf(" %d Image Thumbnails", totals.imageThumbnails)
|
||||
}
|
||||
if logMsg == "Generating" {
|
||||
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))
|
||||
}
|
||||
|
||||
func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) totalsGenerate {
|
||||
var totals totalsGenerate
|
||||
func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) {
|
||||
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
|
||||
|
||||
findFilter := models.BatchFindFilter(batchSize)
|
||||
|
@ -234,26 +246,26 @@ func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, que
|
|||
|
||||
for more := true; more; {
|
||||
if job.IsCancelled(ctx) {
|
||||
return totals
|
||||
return
|
||||
}
|
||||
|
||||
scenes, err := scene.Query(ctx, r.Scene, nil, findFilter)
|
||||
if err != nil {
|
||||
logger.Errorf("Error encountered queuing files to scan: %s", err.Error())
|
||||
return totals
|
||||
return
|
||||
}
|
||||
|
||||
for _, ss := range scenes {
|
||||
if job.IsCancelled(ctx) {
|
||||
return totals
|
||||
return
|
||||
}
|
||||
|
||||
if err := ss.LoadFiles(ctx, r.Scene); err != nil {
|
||||
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 {
|
||||
|
@ -262,30 +274,37 @@ func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, que
|
|||
*findFilter.Page++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*findFilter.Page = 1
|
||||
for more := j.input.ClipPreviews; more; {
|
||||
func (j *GenerateJob) queueImagesTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) {
|
||||
const batchSize = 1000
|
||||
|
||||
findFilter := models.BatchFindFilter(batchSize)
|
||||
|
||||
r := j.repository
|
||||
|
||||
for more := j.input.ClipPreviews || j.input.ImageThumbnails; more; {
|
||||
if job.IsCancelled(ctx) {
|
||||
return totals
|
||||
return
|
||||
}
|
||||
|
||||
images, err := image.Query(ctx, r.Image, nil, findFilter)
|
||||
if err != nil {
|
||||
logger.Errorf("Error encountered queuing files to scan: %s", err.Error())
|
||||
return totals
|
||||
return
|
||||
}
|
||||
|
||||
for _, ss := range images {
|
||||
if job.IsCancelled(ctx) {
|
||||
return totals
|
||||
return
|
||||
}
|
||||
|
||||
if err := ss.LoadFiles(ctx, r.Image); err != nil {
|
||||
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 {
|
||||
|
@ -294,8 +313,6 @@ func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, que
|
|||
*findFilter.Page++
|
||||
}
|
||||
}
|
||||
|
||||
return totals
|
||||
}
|
||||
|
||||
func getGeneratePreviewOptions(optionsInput GeneratePreviewOptionsInput) generate.PreviewOptions {
|
||||
|
@ -333,7 +350,7 @@ func getGeneratePreviewOptions(optionsInput GeneratePreviewOptionsInput) generat
|
|||
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
|
||||
|
||||
if j.input.Covers {
|
||||
|
@ -344,8 +361,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
|
|||
}
|
||||
|
||||
if task.required(ctx) {
|
||||
totals.covers++
|
||||
totals.tasks++
|
||||
j.totals.covers++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
}
|
||||
|
@ -358,8 +375,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
|
|||
}
|
||||
|
||||
if task.required() {
|
||||
totals.sprites++
|
||||
totals.tasks++
|
||||
j.totals.sprites++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
}
|
||||
|
@ -382,13 +399,13 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
|
|||
|
||||
if task.required() {
|
||||
if task.videoPreviewRequired() {
|
||||
totals.previews++
|
||||
j.totals.previews++
|
||||
}
|
||||
if task.imagePreviewRequired() {
|
||||
totals.imagePreviews++
|
||||
j.totals.imagePreviews++
|
||||
}
|
||||
|
||||
totals.tasks++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
}
|
||||
|
@ -407,8 +424,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
|
|||
|
||||
markers := task.markersNeeded(ctx)
|
||||
if markers > 0 {
|
||||
totals.markers += int64(markers)
|
||||
totals.tasks++
|
||||
j.totals.markers += int64(markers)
|
||||
j.totals.tasks++
|
||||
|
||||
queue <- task
|
||||
}
|
||||
|
@ -424,8 +441,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
|
|||
g: g,
|
||||
}
|
||||
if task.required() {
|
||||
totals.transcodes++
|
||||
totals.tasks++
|
||||
j.totals.transcodes++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
}
|
||||
|
@ -441,8 +458,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
|
|||
}
|
||||
|
||||
if task.required() {
|
||||
totals.phashes++
|
||||
totals.tasks++
|
||||
j.totals.phashes++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
}
|
||||
|
@ -457,14 +474,14 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
|
|||
}
|
||||
|
||||
if task.required() {
|
||||
totals.interactiveHeatmapSpeeds++
|
||||
totals.tasks++
|
||||
j.totals.interactiveHeatmapSpeeds++
|
||||
j.totals.tasks++
|
||||
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{
|
||||
repository: j.repository,
|
||||
Marker: marker,
|
||||
|
@ -472,20 +489,35 @@ func (j *GenerateJob) queueMarkerJob(g *generate.Generator, marker *models.Scene
|
|||
fileNamingAlgorithm: j.fileNamingAlgo,
|
||||
generator: g,
|
||||
}
|
||||
totals.markers++
|
||||
totals.tasks++
|
||||
j.totals.markers++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
|
||||
func (j *GenerateJob) queueImageJob(g *generate.Generator, image *models.Image, queue chan<- Task, totals *totalsGenerate) {
|
||||
task := &GenerateClipPreviewTask{
|
||||
Image: *image,
|
||||
Overwrite: j.overwrite,
|
||||
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 task.required() {
|
||||
totals.clipPreviews++
|
||||
totals.tasks++
|
||||
queue <- task
|
||||
if j.input.ClipPreviews {
|
||||
task := &GenerateClipPreviewTask{
|
||||
Image: *image,
|
||||
Overwrite: j.overwrite,
|
||||
}
|
||||
|
||||
if task.required() {
|
||||
j.totals.clipPreviews++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -2,7 +2,6 @@ package manager
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
|
@ -412,9 +411,12 @@ func (g *imageGenerators) Generate(ctx context.Context, i *models.Image, f model
|
|||
|
||||
if t.ScanGenerateThumbnails {
|
||||
// this should be quick, so always generate sequentially
|
||||
if err := g.generateThumbnail(ctx, i, f); err != nil {
|
||||
logger.Errorf("Error generating thumbnail for %s: %v", path, err)
|
||||
taskThumbnail := GenerateImageThumbnailTask{
|
||||
Image: *i,
|
||||
Overwrite: overwrite,
|
||||
}
|
||||
|
||||
taskThumbnail.Start(ctx)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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 {
|
||||
input ScanMetadataInput
|
||||
taskQueue *job.TaskQueue
|
||||
|
|
|
@ -18,6 +18,7 @@ type GenerateMetadataOptions struct {
|
|||
Transcodes bool `json:"transcodes"`
|
||||
Phashes bool `json:"phashes"`
|
||||
InteractiveHeatmapsSpeeds bool `json:"interactiveHeatmapsSpeeds"`
|
||||
ImageThumbnails bool `json:"imageThumbnails"`
|
||||
ClipPreviews bool `json:"clipPreviews"`
|
||||
}
|
||||
|
||||
|
|
|
@ -196,6 +196,7 @@ fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult {
|
|||
phashes
|
||||
interactiveHeatmapsSpeeds
|
||||
clipPreviews
|
||||
imageThumbnails
|
||||
}
|
||||
|
||||
deleteFile
|
||||
|
|
|
@ -17,11 +17,13 @@ import { SettingsContext } from "../Settings/context";
|
|||
interface ISceneGenerateDialog {
|
||||
selectedIds?: string[];
|
||||
onClose: () => void;
|
||||
type: "scene"; // TODO - add image generate
|
||||
}
|
||||
|
||||
export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
||||
selectedIds,
|
||||
onClose,
|
||||
type,
|
||||
}) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
|
||||
|
@ -200,6 +202,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
|||
<SettingsContext>
|
||||
<SettingSection>
|
||||
<GenerateOptions
|
||||
type={type}
|
||||
options={options}
|
||||
setOptions={setOptions}
|
||||
selection
|
||||
|
|
|
@ -278,6 +278,7 @@ const ScenePage: React.FC<IProps> = ({
|
|||
onClose={() => {
|
||||
setIsGenerateDialogOpen(false);
|
||||
}}
|
||||
type="scene"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -235,6 +235,7 @@ export const SceneList: React.FC<ISceneList> = ({
|
|||
if (isGenerateDialogOpen) {
|
||||
return (
|
||||
<GenerateDialog
|
||||
type="scene"
|
||||
selectedIds={Array.from(selectedIds.values())}
|
||||
onClose={() => setIsGenerateDialogOpen(false)}
|
||||
/>
|
||||
|
|
|
@ -7,12 +7,14 @@ import {
|
|||
} from "../GeneratePreviewOptions";
|
||||
|
||||
interface IGenerateOptions {
|
||||
type?: "scene" | "image";
|
||||
selection?: boolean;
|
||||
options: GQL.GenerateMetadataInput;
|
||||
setOptions: (s: GQL.GenerateMetadataInput) => void;
|
||||
}
|
||||
|
||||
export const GenerateOptions: React.FC<IGenerateOptions> = ({
|
||||
type,
|
||||
selection,
|
||||
options,
|
||||
setOptions: setOptionsState,
|
||||
|
@ -24,136 +26,153 @@ export const GenerateOptions: React.FC<IGenerateOptions> = ({
|
|||
setOptionsState({ ...options, ...input });
|
||||
}
|
||||
|
||||
const showSceneOptions = !type || type === "scene";
|
||||
const showImageOptions = !type || type === "image";
|
||||
|
||||
return (
|
||||
<>
|
||||
<BooleanSetting
|
||||
id="covers-task"
|
||||
headingID="dialogs.scene_gen.covers"
|
||||
checked={options.covers ?? false}
|
||||
onChange={(v) => setOptions({ covers: v })}
|
||||
/>
|
||||
<BooleanSetting
|
||||
id="preview-task"
|
||||
checked={options.previews ?? false}
|
||||
headingID="dialogs.scene_gen.video_previews"
|
||||
tooltipID="dialogs.scene_gen.video_previews_tooltip"
|
||||
onChange={(v) => setOptions({ previews: v })}
|
||||
/>
|
||||
<BooleanSetting
|
||||
advanced
|
||||
className="sub-setting"
|
||||
id="image-preview-task"
|
||||
checked={options.imagePreviews ?? false}
|
||||
disabled={!options.previews}
|
||||
headingID="dialogs.scene_gen.image_previews"
|
||||
tooltipID="dialogs.scene_gen.image_previews_tooltip"
|
||||
onChange={(v) => setOptions({ imagePreviews: v })}
|
||||
/>
|
||||
{showSceneOptions && (
|
||||
<>
|
||||
<BooleanSetting
|
||||
id="covers-task"
|
||||
headingID="dialogs.scene_gen.covers"
|
||||
checked={options.covers ?? false}
|
||||
onChange={(v) => setOptions({ covers: v })}
|
||||
/>
|
||||
<BooleanSetting
|
||||
id="preview-task"
|
||||
checked={options.previews ?? false}
|
||||
headingID="dialogs.scene_gen.video_previews"
|
||||
tooltipID="dialogs.scene_gen.video_previews_tooltip"
|
||||
onChange={(v) => setOptions({ previews: v })}
|
||||
/>
|
||||
<BooleanSetting
|
||||
advanced
|
||||
className="sub-setting"
|
||||
id="image-preview-task"
|
||||
checked={options.imagePreviews ?? false}
|
||||
disabled={!options.previews}
|
||||
headingID="dialogs.scene_gen.image_previews"
|
||||
tooltipID="dialogs.scene_gen.image_previews_tooltip"
|
||||
onChange={(v) => setOptions({ imagePreviews: v })}
|
||||
/>
|
||||
|
||||
{/* #2251 - only allow preview generation options to be overridden when generating from a selection */}
|
||||
{selection ? (
|
||||
<ModalSetting<VideoPreviewSettingsInput>
|
||||
id="video-preview-settings"
|
||||
className="sub-setting"
|
||||
disabled={!options.previews}
|
||||
headingID="dialogs.scene_gen.override_preview_generation_options"
|
||||
tooltipID="dialogs.scene_gen.override_preview_generation_options_desc"
|
||||
value={{
|
||||
previewExcludeEnd: previewOptions.previewExcludeEnd,
|
||||
previewExcludeStart: previewOptions.previewExcludeStart,
|
||||
previewSegmentDuration: previewOptions.previewSegmentDuration,
|
||||
previewSegments: previewOptions.previewSegments,
|
||||
}}
|
||||
onChange={(v) => setOptions({ previewOptions: v })}
|
||||
renderField={(value, setValue) => (
|
||||
<VideoPreviewInput value={value ?? {}} setValue={setValue} />
|
||||
)}
|
||||
renderValue={() => {
|
||||
return <></>;
|
||||
}}
|
||||
/>
|
||||
) : undefined}
|
||||
{/* #2251 - only allow preview generation options to be overridden when generating from a selection */}
|
||||
{selection ? (
|
||||
<ModalSetting<VideoPreviewSettingsInput>
|
||||
id="video-preview-settings"
|
||||
className="sub-setting"
|
||||
disabled={!options.previews}
|
||||
headingID="dialogs.scene_gen.override_preview_generation_options"
|
||||
tooltipID="dialogs.scene_gen.override_preview_generation_options_desc"
|
||||
value={{
|
||||
previewExcludeEnd: previewOptions.previewExcludeEnd,
|
||||
previewExcludeStart: previewOptions.previewExcludeStart,
|
||||
previewSegmentDuration: previewOptions.previewSegmentDuration,
|
||||
previewSegments: previewOptions.previewSegments,
|
||||
}}
|
||||
onChange={(v) => setOptions({ previewOptions: v })}
|
||||
renderField={(value, setValue) => (
|
||||
<VideoPreviewInput value={value ?? {}} setValue={setValue} />
|
||||
)}
|
||||
renderValue={() => {
|
||||
return <></>;
|
||||
}}
|
||||
/>
|
||||
) : undefined}
|
||||
|
||||
<BooleanSetting
|
||||
id="sprite-task"
|
||||
checked={options.sprites ?? false}
|
||||
headingID="dialogs.scene_gen.sprites"
|
||||
tooltipID="dialogs.scene_gen.sprites_tooltip"
|
||||
onChange={(v) => setOptions({ sprites: v })}
|
||||
/>
|
||||
<BooleanSetting
|
||||
id="marker-task"
|
||||
checked={options.markers ?? false}
|
||||
headingID="dialogs.scene_gen.markers"
|
||||
tooltipID="dialogs.scene_gen.markers_tooltip"
|
||||
onChange={(v) => setOptions({ markers: v })}
|
||||
/>
|
||||
<BooleanSetting
|
||||
advanced
|
||||
id="marker-image-preview-task"
|
||||
className="sub-setting"
|
||||
checked={options.markerImagePreviews ?? false}
|
||||
disabled={!options.markers}
|
||||
headingID="dialogs.scene_gen.marker_image_previews"
|
||||
tooltipID="dialogs.scene_gen.marker_image_previews_tooltip"
|
||||
onChange={(v) =>
|
||||
setOptions({
|
||||
markerImagePreviews: v,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<BooleanSetting
|
||||
advanced
|
||||
id="marker-screenshot-task"
|
||||
className="sub-setting"
|
||||
checked={options.markerScreenshots ?? false}
|
||||
disabled={!options.markers}
|
||||
headingID="dialogs.scene_gen.marker_screenshots"
|
||||
tooltipID="dialogs.scene_gen.marker_screenshots_tooltip"
|
||||
onChange={(v) => setOptions({ markerScreenshots: v })}
|
||||
/>
|
||||
<BooleanSetting
|
||||
id="sprite-task"
|
||||
checked={options.sprites ?? false}
|
||||
headingID="dialogs.scene_gen.sprites"
|
||||
tooltipID="dialogs.scene_gen.sprites_tooltip"
|
||||
onChange={(v) => setOptions({ sprites: v })}
|
||||
/>
|
||||
<BooleanSetting
|
||||
id="marker-task"
|
||||
checked={options.markers ?? false}
|
||||
headingID="dialogs.scene_gen.markers"
|
||||
tooltipID="dialogs.scene_gen.markers_tooltip"
|
||||
onChange={(v) => setOptions({ markers: v })}
|
||||
/>
|
||||
<BooleanSetting
|
||||
advanced
|
||||
id="marker-image-preview-task"
|
||||
className="sub-setting"
|
||||
checked={options.markerImagePreviews ?? false}
|
||||
disabled={!options.markers}
|
||||
headingID="dialogs.scene_gen.marker_image_previews"
|
||||
tooltipID="dialogs.scene_gen.marker_image_previews_tooltip"
|
||||
onChange={(v) =>
|
||||
setOptions({
|
||||
markerImagePreviews: v,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<BooleanSetting
|
||||
advanced
|
||||
id="marker-screenshot-task"
|
||||
className="sub-setting"
|
||||
checked={options.markerScreenshots ?? false}
|
||||
disabled={!options.markers}
|
||||
headingID="dialogs.scene_gen.marker_screenshots"
|
||||
tooltipID="dialogs.scene_gen.marker_screenshots_tooltip"
|
||||
onChange={(v) => setOptions({ markerScreenshots: v })}
|
||||
/>
|
||||
|
||||
<BooleanSetting
|
||||
advanced
|
||||
id="transcode-task"
|
||||
checked={options.transcodes ?? false}
|
||||
headingID="dialogs.scene_gen.transcodes"
|
||||
tooltipID="dialogs.scene_gen.transcodes_tooltip"
|
||||
onChange={(v) => setOptions({ transcodes: v })}
|
||||
/>
|
||||
{selection ? (
|
||||
<BooleanSetting
|
||||
advanced
|
||||
id="force-transcode"
|
||||
className="sub-setting"
|
||||
checked={options.forceTranscodes ?? false}
|
||||
disabled={!options.transcodes}
|
||||
headingID="dialogs.scene_gen.force_transcodes"
|
||||
tooltipID="dialogs.scene_gen.force_transcodes_tooltip"
|
||||
onChange={(v) => setOptions({ forceTranscodes: v })}
|
||||
/>
|
||||
) : undefined}
|
||||
<BooleanSetting
|
||||
advanced
|
||||
id="transcode-task"
|
||||
checked={options.transcodes ?? false}
|
||||
headingID="dialogs.scene_gen.transcodes"
|
||||
tooltipID="dialogs.scene_gen.transcodes_tooltip"
|
||||
onChange={(v) => setOptions({ transcodes: v })}
|
||||
/>
|
||||
{selection ? (
|
||||
<BooleanSetting
|
||||
advanced
|
||||
id="force-transcode"
|
||||
className="sub-setting"
|
||||
checked={options.forceTranscodes ?? false}
|
||||
disabled={!options.transcodes}
|
||||
headingID="dialogs.scene_gen.force_transcodes"
|
||||
tooltipID="dialogs.scene_gen.force_transcodes_tooltip"
|
||||
onChange={(v) => setOptions({ forceTranscodes: v })}
|
||||
/>
|
||||
) : undefined}
|
||||
|
||||
<BooleanSetting
|
||||
id="phash-task"
|
||||
checked={options.phashes ?? false}
|
||||
headingID="dialogs.scene_gen.phash"
|
||||
tooltipID="dialogs.scene_gen.phash_tooltip"
|
||||
onChange={(v) => setOptions({ phashes: v })}
|
||||
/>
|
||||
<BooleanSetting
|
||||
id="phash-task"
|
||||
checked={options.phashes ?? false}
|
||||
headingID="dialogs.scene_gen.phash"
|
||||
tooltipID="dialogs.scene_gen.phash_tooltip"
|
||||
onChange={(v) => setOptions({ phashes: v })}
|
||||
/>
|
||||
|
||||
<BooleanSetting
|
||||
id="interactive-heatmap-speed-task"
|
||||
checked={options.interactiveHeatmapsSpeeds ?? false}
|
||||
headingID="dialogs.scene_gen.interactive_heatmap_speed"
|
||||
onChange={(v) => setOptions({ interactiveHeatmapsSpeeds: v })}
|
||||
/>
|
||||
<BooleanSetting
|
||||
id="clip-previews"
|
||||
checked={options.clipPreviews ?? false}
|
||||
headingID="dialogs.scene_gen.clip_previews"
|
||||
onChange={(v) => setOptions({ clipPreviews: v })}
|
||||
/>
|
||||
<BooleanSetting
|
||||
id="interactive-heatmap-speed-task"
|
||||
checked={options.interactiveHeatmapsSpeeds ?? false}
|
||||
headingID="dialogs.scene_gen.interactive_heatmap_speed"
|
||||
onChange={(v) => setOptions({ interactiveHeatmapsSpeeds: v })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{showImageOptions && (
|
||||
<>
|
||||
<BooleanSetting
|
||||
id="clip-previews"
|
||||
checked={options.clipPreviews ?? false}
|
||||
headingID="dialogs.scene_gen.clip_previews"
|
||||
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
|
||||
id="overwrite"
|
||||
checked={options.overwrite ?? false}
|
||||
|
|
|
@ -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.",
|
||||
"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_thumbnails": "Image Thumbnails",
|
||||
"interactive_heatmap_speed": "Generate heatmaps and speeds for interactive scenes",
|
||||
"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.",
|
||||
|
|
Loading…
Reference in New Issue