Generate content for specific scenes (#672)

* Add UI dialog for scene(s)
* Move preview preset to config
This commit is contained in:
WithoutPants 2020-07-19 11:59:18 +10:00 committed by GitHub
parent 8e4945325d
commit c104c6d075
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 552 additions and 148 deletions

View file

@ -3,6 +3,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
databasePath databasePath
generatedPath generatedPath
cachePath cachePath
previewPreset
maxTranscodeSize maxTranscodeSize
maxStreamingTranscodeSize maxStreamingTranscodeSize
forceMkv forceMkv

View file

@ -7,6 +7,16 @@ enum StreamingResolutionEnum {
"Original", ORIGINAL "Original", ORIGINAL
} }
enum PreviewPreset {
"X264_ULTRAFAST", ultrafast
"X264_VERYFAST", veryfast
"X264_FAST", fast
"X264_MEDIUM", medium
"X264_SLOW", slow
"X264_SLOWER", slower
"X264_VERYSLOW", veryslow
}
input ConfigGeneralInput { input ConfigGeneralInput {
"""Array of file paths to content""" """Array of file paths to content"""
stashes: [String!] stashes: [String!]
@ -16,6 +26,8 @@ input ConfigGeneralInput {
generatedPath: String generatedPath: String
"""Path to cache""" """Path to cache"""
cachePath: String cachePath: String
"""Preset when generating preview"""
previewPreset: PreviewPreset
"""Max generated transcode size""" """Max generated transcode size"""
maxTranscodeSize: StreamingResolutionEnum maxTranscodeSize: StreamingResolutionEnum
"""Max streaming transcode size""" """Max streaming transcode size"""
@ -53,6 +65,8 @@ type ConfigGeneralResult {
generatedPath: String! generatedPath: String!
"""Path to cache""" """Path to cache"""
cachePath: String! cachePath: String!
"""Preset when generating preview"""
previewPreset: PreviewPreset!
"""Max generated transcode size""" """Max generated transcode size"""
maxTranscodeSize: StreamingResolutionEnum maxTranscodeSize: StreamingResolutionEnum
"""Max streaming transcode size""" """Max streaming transcode size"""

View file

@ -1,12 +1,21 @@
input GenerateMetadataInput { input GenerateMetadataInput {
sprites: Boolean! sprites: Boolean!
previews: Boolean! previews: Boolean!
previewPreset: PreviewPreset
imagePreviews: Boolean! imagePreviews: Boolean!
markers: Boolean! markers: Boolean!
transcodes: Boolean! transcodes: Boolean!
"""gallery thumbnails for cache usage""" """gallery thumbnails for cache usage"""
thumbnails: Boolean! thumbnails: Boolean!
"""scene ids to generate for"""
sceneIDs: [ID!]
"""marker ids to generate for"""
markerIDs: [ID!]
"""gallery ids to generate for"""
galleryIDs: [ID!]
"""overwrite existing media"""
overwrite: Boolean
} }
input ScanMetadataInput { input ScanMetadataInput {
@ -27,13 +36,3 @@ type MetadataUpdateStatus {
status: String! status: String!
message: String! message: String!
} }
enum PreviewPreset {
"X264_ULTRAFAST", ultrafast
"X264_VERYFAST", veryfast
"X264_FAST", fast
"X264_MEDIUM", medium
"X264_SLOW", slow
"X264_SLOWER", slower
"X264_VERYSLOW", veryslow
}

View file

@ -45,6 +45,10 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
config.Set(config.Cache, input.CachePath) config.Set(config.Cache, input.CachePath)
} }
if input.PreviewPreset != nil {
config.Set(config.PreviewPreset, input.PreviewPreset.String())
}
if input.MaxTranscodeSize != nil { if input.MaxTranscodeSize != nil {
config.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String()) config.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
} }

View file

@ -23,7 +23,7 @@ func (r *mutationResolver) MetadataExport(ctx context.Context) (string, error) {
} }
func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.GenerateMetadataInput) (string, error) { func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.GenerateMetadataInput) (string, error) {
manager.GetInstance().Generate(input.Sprites, input.Previews, input.PreviewPreset, input.ImagePreviews, input.Markers, input.Transcodes, input.Thumbnails) manager.GetInstance().Generate(input)
return "todo", nil return "todo", nil
} }

View file

@ -46,6 +46,7 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
DatabasePath: config.GetDatabasePath(), DatabasePath: config.GetDatabasePath(),
GeneratedPath: config.GetGeneratedPath(), GeneratedPath: config.GetGeneratedPath(),
CachePath: config.GetCachePath(), CachePath: config.GetCachePath(),
PreviewPreset: config.GetPreviewPreset(),
MaxTranscodeSize: &maxTranscodeSize, MaxTranscodeSize: &maxTranscodeSize,
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize, MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
ForceMkv: config.GetForceMKV(), ForceMkv: config.GetForceMKV(),

View file

@ -27,6 +27,8 @@ const Database = "database"
const Exclude = "exclude" const Exclude = "exclude"
const PreviewPreset = "preview_preset"
const MaxTranscodeSize = "max_transcode_size" const MaxTranscodeSize = "max_transcode_size"
const MaxStreamingTranscodeSize = "max_streaming_transcode_size" const MaxStreamingTranscodeSize = "max_streaming_transcode_size"
@ -160,6 +162,19 @@ func GetExternalHost() string {
return viper.GetString(ExternalHost) return viper.GetString(ExternalHost)
} }
// GetPreviewPreset returns the preset when generating previews. Defaults to
// Slow.
func GetPreviewPreset() models.PreviewPreset {
ret := viper.GetString(PreviewPreset)
// default to slow
if ret == "" {
return models.PreviewPresetSlow
}
return models.PreviewPreset(ret)
}
func GetMaxTranscodeSize() models.StreamingResolutionEnum { func GetMaxTranscodeSize() models.StreamingResolutionEnum {
ret := viper.GetString(MaxTranscodeSize) ret := viper.GetString(MaxTranscodeSize)

View file

@ -3,11 +3,12 @@ package manager
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"os"
"path/filepath"
"github.com/stashapp/stash/pkg/ffmpeg" "github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/utils" "github.com/stashapp/stash/pkg/utils"
"os"
"path/filepath"
) )
type PreviewGenerator struct { type PreviewGenerator struct {
@ -21,6 +22,8 @@ type PreviewGenerator struct {
GenerateImage bool GenerateImage bool
PreviewPreset string PreviewPreset string
Overwrite bool
} }
func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, imageFilename string, outputDirectory string, generateVideo bool, generateImage bool, previewPreset string) (*PreviewGenerator, error) { func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, imageFilename string, outputDirectory string, generateVideo bool, generateImage bool, previewPreset string) (*PreviewGenerator, error) {
@ -88,7 +91,7 @@ func (g *PreviewGenerator) generateConcatFile() error {
func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error { func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error {
outputPath := filepath.Join(g.OutputDirectory, g.VideoFilename) outputPath := filepath.Join(g.OutputDirectory, g.VideoFilename)
outputExists, _ := utils.FileExists(outputPath) outputExists, _ := utils.FileExists(outputPath)
if outputExists { if !g.Overwrite && outputExists {
return nil return nil
} }
@ -116,7 +119,7 @@ func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error {
func (g *PreviewGenerator) generateImage(encoder *ffmpeg.Encoder) error { func (g *PreviewGenerator) generateImage(encoder *ffmpeg.Encoder) error {
outputPath := filepath.Join(g.OutputDirectory, g.ImageFilename) outputPath := filepath.Join(g.OutputDirectory, g.ImageFilename)
outputExists, _ := utils.FileExists(outputPath) outputExists, _ := utils.FileExists(outputPath)
if outputExists { if !g.Overwrite && outputExists {
return nil return nil
} }

View file

@ -2,17 +2,18 @@ package manager
import ( import (
"fmt" "fmt"
"github.com/bmatcuk/doublestar"
"github.com/disintegration/imaging"
"github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/utils"
"image" "image"
"image/color" "image/color"
"io/ioutil" "io/ioutil"
"math" "math"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/bmatcuk/doublestar"
"github.com/disintegration/imaging"
"github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/utils"
) )
type SpriteGenerator struct { type SpriteGenerator struct {
@ -22,6 +23,8 @@ type SpriteGenerator struct {
VTTOutputPath string VTTOutputPath string
Rows int Rows int
Columns int Columns int
Overwrite bool
} }
func NewSpriteGenerator(videoFile ffmpeg.VideoFile, imageOutputPath string, vttOutputPath string, rows int, cols int) (*SpriteGenerator, error) { func NewSpriteGenerator(videoFile ffmpeg.VideoFile, imageOutputPath string, vttOutputPath string, rows int, cols int) (*SpriteGenerator, error) {
@ -60,7 +63,7 @@ func (g *SpriteGenerator) Generate() error {
} }
func (g *SpriteGenerator) generateSpriteImage(encoder *ffmpeg.Encoder) error { func (g *SpriteGenerator) generateSpriteImage(encoder *ffmpeg.Encoder) error {
if g.imageExists() { if !g.Overwrite && g.imageExists() {
return nil return nil
} }
logger.Infof("[generator] generating sprite image for %s", g.Info.VideoFile.Path) logger.Infof("[generator] generating sprite image for %s", g.Info.VideoFile.Path)
@ -112,7 +115,7 @@ func (g *SpriteGenerator) generateSpriteImage(encoder *ffmpeg.Encoder) error {
} }
func (g *SpriteGenerator) generateSpriteVTT(encoder *ffmpeg.Encoder) error { func (g *SpriteGenerator) generateSpriteVTT(encoder *ffmpeg.Encoder) error {
if g.vttExists() { if !g.Overwrite && g.vttExists() {
return nil return nil
} }
logger.Infof("[generator] generating sprite vtt for %s", g.Info.VideoFile.Path) logger.Infof("[generator] generating sprite vtt for %s", g.Info.VideoFile.Path)

View file

@ -167,7 +167,7 @@ func (s *singleton) Export() {
}() }()
} }
func (s *singleton) Generate(sprites bool, previews bool, previewPreset *models.PreviewPreset, imagePreviews bool, markers bool, transcodes bool, thumbnails bool) { func (s *singleton) Generate(input models.GenerateMetadataInput) {
if s.Status.Status != Idle { if s.Status.Status != Idle {
return return
} }
@ -176,32 +176,49 @@ func (s *singleton) Generate(sprites bool, previews bool, previewPreset *models.
qb := models.NewSceneQueryBuilder() qb := models.NewSceneQueryBuilder()
qg := models.NewGalleryQueryBuilder() qg := models.NewGalleryQueryBuilder()
mqb := models.NewSceneMarkerQueryBuilder()
//this.job.total = await ObjectionUtils.getCount(Scene); //this.job.total = await ObjectionUtils.getCount(Scene);
instance.Paths.Generated.EnsureTmpDir() instance.Paths.Generated.EnsureTmpDir()
preset := string(models.PreviewPresetSlow) preset := config.GetPreviewPreset().String()
if previewPreset != nil && previewPreset.IsValid() {
preset = string(*previewPreset) galleryIDs := utils.StringSliceToIntSlice(input.GalleryIDs)
} sceneIDs := utils.StringSliceToIntSlice(input.SceneIDs)
markerIDs := utils.StringSliceToIntSlice(input.MarkerIDs)
go func() { go func() {
defer s.returnToIdleState() defer s.returnToIdleState()
scenes, err := qb.All() var scenes []*models.Scene
var galleries []*models.Gallery var err error
if len(sceneIDs) > 0 {
scenes, err = qb.FindMany(sceneIDs)
} else {
scenes, err = qb.All()
}
if err != nil { if err != nil {
logger.Errorf("failed to get scenes for generate") logger.Errorf("failed to get scenes for generate")
return return
} }
delta := utils.Btoi(sprites) + utils.Btoi(previews) + utils.Btoi(markers) + utils.Btoi(transcodes) delta := utils.Btoi(input.Sprites) + utils.Btoi(input.Previews) + utils.Btoi(input.Markers) + utils.Btoi(input.Transcodes)
var wg sync.WaitGroup var wg sync.WaitGroup
s.Status.Progress = 0 s.Status.Progress = 0
lenScenes := len(scenes) lenScenes := len(scenes)
total := lenScenes total := lenScenes
if thumbnails {
var galleries []*models.Gallery
if input.Thumbnails {
if len(galleryIDs) > 0 {
galleries, err = qg.FindMany(galleryIDs)
} else {
galleries, err = qg.All() galleries, err = qg.All()
}
if err != nil { if err != nil {
logger.Errorf("failed to get galleries for generate") logger.Errorf("failed to get galleries for generate")
return return
@ -209,17 +226,31 @@ func (s *singleton) Generate(sprites bool, previews bool, previewPreset *models.
total += len(galleries) total += len(galleries)
} }
var markers []*models.SceneMarker
if len(markerIDs) > 0 {
markers, err = mqb.FindMany(markerIDs)
total += len(markers)
}
if s.Status.stopping { if s.Status.stopping {
logger.Info("Stopping due to user request") logger.Info("Stopping due to user request")
return return
} }
totalsNeeded := s.neededGenerate(scenes, sprites, previews, imagePreviews, markers, transcodes)
totalsNeeded := s.neededGenerate(scenes, input)
if totalsNeeded == nil { if totalsNeeded == nil {
logger.Infof("Taking too long to count content. Skipping...") logger.Infof("Taking too long to count content. Skipping...")
logger.Infof("Generating content") logger.Infof("Generating content")
} else { } else {
logger.Infof("Generating %d sprites %d previews %d image previews %d markers %d transcodes", totalsNeeded.sprites, totalsNeeded.previews, totalsNeeded.imagePreviews, totalsNeeded.markers, totalsNeeded.transcodes) logger.Infof("Generating %d sprites %d previews %d image previews %d markers %d transcodes", totalsNeeded.sprites, totalsNeeded.previews, totalsNeeded.imagePreviews, totalsNeeded.markers, totalsNeeded.transcodes)
} }
overwrite := false
if input.Overwrite != nil {
overwrite = *input.Overwrite
}
for i, scene := range scenes { for i, scene := range scenes {
s.Status.setProgress(i, total) s.Status.setProgress(i, total)
if s.Status.stopping { if s.Status.stopping {
@ -235,34 +266,34 @@ func (s *singleton) Generate(sprites bool, previews bool, previewPreset *models.
wg.Add(delta) wg.Add(delta)
// Clear the tmp directory for each scene // Clear the tmp directory for each scene
if sprites || previews || markers { if input.Sprites || input.Previews || input.Markers {
instance.Paths.Generated.EmptyTmpDir() instance.Paths.Generated.EmptyTmpDir()
} }
if sprites { if input.Sprites {
task := GenerateSpriteTask{Scene: *scene} task := GenerateSpriteTask{Scene: *scene, Overwrite: overwrite}
go task.Start(&wg) go task.Start(&wg)
} }
if previews { if input.Previews {
task := GeneratePreviewTask{Scene: *scene, ImagePreview: imagePreviews, PreviewPreset: preset} task := GeneratePreviewTask{Scene: *scene, ImagePreview: input.ImagePreviews, PreviewPreset: preset, Overwrite: overwrite}
go task.Start(&wg) go task.Start(&wg)
} }
if markers { if input.Markers {
task := GenerateMarkersTask{Scene: *scene} task := GenerateMarkersTask{Scene: scene, Overwrite: overwrite}
go task.Start(&wg) go task.Start(&wg)
} }
if transcodes { if input.Transcodes {
task := GenerateTranscodeTask{Scene: *scene} task := GenerateTranscodeTask{Scene: *scene, Overwrite: overwrite}
go task.Start(&wg) go task.Start(&wg)
} }
wg.Wait() wg.Wait()
} }
if thumbnails { if input.Thumbnails {
logger.Infof("Generating thumbnails for the galleries") logger.Infof("Generating thumbnails for the galleries")
for i, gallery := range galleries { for i, gallery := range galleries {
s.Status.setProgress(lenScenes+i, total) s.Status.setProgress(lenScenes+i, total)
@ -277,12 +308,30 @@ func (s *singleton) Generate(sprites bool, previews bool, previewPreset *models.
} }
wg.Add(1) wg.Add(1)
task := GenerateGthumbsTask{Gallery: *gallery} task := GenerateGthumbsTask{Gallery: *gallery, Overwrite: overwrite}
go task.Start(&wg) go task.Start(&wg)
wg.Wait() wg.Wait()
} }
} }
for i, marker := range markers {
s.Status.setProgress(lenScenes+len(galleries)+i, total)
if s.Status.stopping {
logger.Info("Stopping due to user request")
return
}
if marker == nil {
logger.Errorf("nil marker, skipping generate")
continue
}
wg.Add(1)
task := GenerateMarkersTask{Marker: marker, Overwrite: overwrite}
go task.Start(&wg)
wg.Wait()
}
logger.Infof("Generate finished") logger.Infof("Generate finished")
}() }()
} }
@ -610,7 +659,7 @@ type totalsGenerate struct {
transcodes int64 transcodes int64
} }
func (s *singleton) neededGenerate(scenes []*models.Scene, sprites, previews, imagePreviews, markers, transcodes bool) *totalsGenerate { func (s *singleton) neededGenerate(scenes []*models.Scene, input models.GenerateMetadataInput) *totalsGenerate {
var totals totalsGenerate var totals totalsGenerate
const timeout = 90 * time.Second const timeout = 90 * time.Second
@ -624,33 +673,38 @@ func (s *singleton) neededGenerate(scenes []*models.Scene, sprites, previews, im
chTimeout <- struct{}{} chTimeout <- struct{}{}
}() }()
overwrite := false
if input.Overwrite != nil {
overwrite = *input.Overwrite
}
logger.Infof("Counting content to generate...") logger.Infof("Counting content to generate...")
for _, scene := range scenes { for _, scene := range scenes {
if scene != nil { if scene != nil {
if sprites { if input.Sprites {
task := GenerateSpriteTask{Scene: *scene} task := GenerateSpriteTask{Scene: *scene}
if !task.doesSpriteExist(task.Scene.Checksum) { if overwrite || !task.doesSpriteExist(task.Scene.Checksum) {
totals.sprites++ totals.sprites++
} }
} }
if previews { if input.Previews {
task := GeneratePreviewTask{Scene: *scene, ImagePreview: imagePreviews} task := GeneratePreviewTask{Scene: *scene, ImagePreview: input.ImagePreviews}
if !task.doesVideoPreviewExist(task.Scene.Checksum) { if overwrite || !task.doesVideoPreviewExist(task.Scene.Checksum) {
totals.previews++ totals.previews++
} }
if imagePreviews && !task.doesImagePreviewExist(task.Scene.Checksum) { if input.ImagePreviews && (overwrite || !task.doesImagePreviewExist(task.Scene.Checksum)) {
totals.imagePreviews++ totals.imagePreviews++
} }
} }
if markers { if input.Markers {
task := GenerateMarkersTask{Scene: *scene} task := GenerateMarkersTask{Scene: scene, Overwrite: overwrite}
totals.markers += int64(task.isMarkerNeeded()) totals.markers += int64(task.isMarkerNeeded())
} }
if transcodes {
task := GenerateTranscodeTask{Scene: *scene} if input.Transcodes {
task := GenerateTranscodeTask{Scene: *scene, Overwrite: overwrite}
if task.isTranscodeNeeded() { if task.isTranscodeNeeded() {
totals.transcodes++ totals.transcodes++
} }

View file

@ -1,15 +1,17 @@
package manager package manager
import ( import (
"sync"
"github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager/paths" "github.com/stashapp/stash/pkg/manager/paths"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils" "github.com/stashapp/stash/pkg/utils"
"sync"
) )
type GenerateGthumbsTask struct { type GenerateGthumbsTask struct {
Gallery models.Gallery Gallery models.Gallery
Overwrite bool
} }
func (t *GenerateGthumbsTask) Start(wg *sync.WaitGroup) { func (t *GenerateGthumbsTask) Start(wg *sync.WaitGroup) {
@ -19,7 +21,7 @@ func (t *GenerateGthumbsTask) Start(wg *sync.WaitGroup) {
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
thumbPath := paths.GetGthumbPath(t.Gallery.Checksum, i, models.DefaultGthumbWidth) thumbPath := paths.GetGthumbPath(t.Gallery.Checksum, i, models.DefaultGthumbWidth)
exists, _ := utils.FileExists(thumbPath) exists, _ := utils.FileExists(thumbPath)
if exists { if !t.Overwrite && exists {
continue continue
} }
data := t.Gallery.GetThumbnail(i, models.DefaultGthumbWidth) data := t.Gallery.GetThumbnail(i, models.DefaultGthumbWidth)

View file

@ -13,12 +13,37 @@ import (
) )
type GenerateMarkersTask struct { type GenerateMarkersTask struct {
Scene models.Scene Scene *models.Scene
Marker *models.SceneMarker
Overwrite bool
} }
func (t *GenerateMarkersTask) Start(wg *sync.WaitGroup) { func (t *GenerateMarkersTask) Start(wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
if t.Scene != nil {
t.generateSceneMarkers()
}
if t.Marker != nil {
qb := models.NewSceneQueryBuilder()
scene, err := qb.Find(int(t.Marker.SceneID.Int64))
if err != nil {
logger.Errorf("error finding scene for marker: %s", err.Error())
return
}
videoFile, err := ffmpeg.NewVideoFile(instance.FFProbePath, t.Scene.Path)
if err != nil {
logger.Errorf("error reading video file: %s", err.Error())
return
}
t.generateMarker(videoFile, scene, t.Marker)
}
}
func (t *GenerateMarkersTask) generateSceneMarkers() {
qb := models.NewSceneMarkerQueryBuilder() qb := models.NewSceneMarkerQueryBuilder()
sceneMarkers, _ := qb.FindBySceneID(t.Scene.ID, nil) sceneMarkers, _ := qb.FindBySceneID(t.Scene.ID, nil)
if len(sceneMarkers) == 0 { if len(sceneMarkers) == 0 {
@ -35,26 +60,33 @@ func (t *GenerateMarkersTask) Start(wg *sync.WaitGroup) {
markersFolder := filepath.Join(instance.Paths.Generated.Markers, t.Scene.Checksum) markersFolder := filepath.Join(instance.Paths.Generated.Markers, t.Scene.Checksum)
_ = utils.EnsureDir(markersFolder) _ = utils.EnsureDir(markersFolder)
encoder := ffmpeg.NewEncoder(instance.FFMPEGPath)
for i, sceneMarker := range sceneMarkers { for i, sceneMarker := range sceneMarkers {
index := i + 1 index := i + 1
logger.Progressf("[generator] <%s> scene marker %d of %d", t.Scene.Checksum, index, len(sceneMarkers)) logger.Progressf("[generator] <%s> scene marker %d of %d", t.Scene.Checksum, index, len(sceneMarkers))
t.generateMarker(videoFile, t.Scene, sceneMarker)
}
}
func (t *GenerateMarkersTask) generateMarker(videoFile *ffmpeg.VideoFile, scene *models.Scene, sceneMarker *models.SceneMarker) {
seconds := int(sceneMarker.Seconds) seconds := int(sceneMarker.Seconds)
baseFilename := strconv.Itoa(seconds) baseFilename := strconv.Itoa(seconds)
videoFilename := baseFilename + ".mp4" videoFilename := baseFilename + ".mp4"
imageFilename := baseFilename + ".webp" imageFilename := baseFilename + ".webp"
videoPath := instance.Paths.SceneMarkers.GetStreamPath(t.Scene.Checksum, seconds) videoPath := instance.Paths.SceneMarkers.GetStreamPath(scene.Checksum, seconds)
imagePath := instance.Paths.SceneMarkers.GetStreamPreviewImagePath(t.Scene.Checksum, seconds) imagePath := instance.Paths.SceneMarkers.GetStreamPreviewImagePath(scene.Checksum, seconds)
videoExists, _ := utils.FileExists(videoPath) videoExists, _ := utils.FileExists(videoPath)
imageExists, _ := utils.FileExists(imagePath) imageExists, _ := utils.FileExists(imagePath)
options := ffmpeg.SceneMarkerOptions{ options := ffmpeg.SceneMarkerOptions{
ScenePath: t.Scene.Path, ScenePath: scene.Path,
Seconds: seconds, Seconds: seconds,
Width: 640, Width: 640,
} }
if !videoExists {
encoder := ffmpeg.NewEncoder(instance.FFMPEGPath)
if t.Overwrite || !videoExists {
options.OutputPath = instance.Paths.Generated.GetTmpPath(videoFilename) // tmp output in case the process ends abruptly options.OutputPath = instance.Paths.Generated.GetTmpPath(videoFilename) // tmp output in case the process ends abruptly
if err := encoder.SceneMarkerVideo(*videoFile, options); err != nil { if err := encoder.SceneMarkerVideo(*videoFile, options); err != nil {
logger.Errorf("[generator] failed to generate marker video: %s", err) logger.Errorf("[generator] failed to generate marker video: %s", err)
@ -64,7 +96,7 @@ func (t *GenerateMarkersTask) Start(wg *sync.WaitGroup) {
} }
} }
if !imageExists { if t.Overwrite || !imageExists {
options.OutputPath = instance.Paths.Generated.GetTmpPath(imageFilename) // tmp output in case the process ends abruptly options.OutputPath = instance.Paths.Generated.GetTmpPath(imageFilename) // tmp output in case the process ends abruptly
if err := encoder.SceneMarkerImage(*videoFile, options); err != nil { if err := encoder.SceneMarkerImage(*videoFile, options); err != nil {
logger.Errorf("[generator] failed to generate marker image: %s", err) logger.Errorf("[generator] failed to generate marker image: %s", err)
@ -73,7 +105,6 @@ func (t *GenerateMarkersTask) Start(wg *sync.WaitGroup) {
logger.Debug("created marker image: ", videoPath) logger.Debug("created marker image: ", videoPath)
} }
} }
}
} }
func (t *GenerateMarkersTask) isMarkerNeeded() int { func (t *GenerateMarkersTask) isMarkerNeeded() int {
@ -92,10 +123,11 @@ func (t *GenerateMarkersTask) isMarkerNeeded() int {
videoExists, _ := utils.FileExists(videoPath) videoExists, _ := utils.FileExists(videoPath)
imageExists, _ := utils.FileExists(imagePath) imageExists, _ := utils.FileExists(imagePath)
if (!videoExists) || (!imageExists) { if t.Overwrite || !videoExists || !imageExists {
markers++ markers++
} }
} }
return markers return markers
} }

View file

@ -1,17 +1,19 @@
package manager package manager
import ( import (
"sync"
"github.com/stashapp/stash/pkg/ffmpeg" "github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils" "github.com/stashapp/stash/pkg/utils"
"sync"
) )
type GeneratePreviewTask struct { type GeneratePreviewTask struct {
Scene models.Scene Scene models.Scene
ImagePreview bool ImagePreview bool
PreviewPreset string PreviewPreset string
Overwrite bool
} }
func (t *GeneratePreviewTask) Start(wg *sync.WaitGroup) { func (t *GeneratePreviewTask) Start(wg *sync.WaitGroup) {
@ -20,7 +22,7 @@ func (t *GeneratePreviewTask) Start(wg *sync.WaitGroup) {
videoFilename := t.videoFilename() videoFilename := t.videoFilename()
imageFilename := t.imageFilename() imageFilename := t.imageFilename()
videoExists := t.doesVideoPreviewExist(t.Scene.Checksum) videoExists := t.doesVideoPreviewExist(t.Scene.Checksum)
if (!t.ImagePreview || t.doesImagePreviewExist(t.Scene.Checksum)) && videoExists { if !t.Overwrite && ((!t.ImagePreview || t.doesImagePreviewExist(t.Scene.Checksum)) && videoExists) {
return return
} }
@ -30,11 +32,12 @@ func (t *GeneratePreviewTask) Start(wg *sync.WaitGroup) {
return return
} }
generator, err := NewPreviewGenerator(*videoFile, videoFilename, imageFilename, instance.Paths.Generated.Screenshots, !videoExists, t.ImagePreview, t.PreviewPreset) generator, err := NewPreviewGenerator(*videoFile, videoFilename, imageFilename, instance.Paths.Generated.Screenshots, true, t.ImagePreview, t.PreviewPreset)
if err != nil { if err != nil {
logger.Errorf("error creating preview generator: %s", err.Error()) logger.Errorf("error creating preview generator: %s", err.Error())
return return
} }
generator.Overwrite = t.Overwrite
if err := generator.Generate(); err != nil { if err := generator.Generate(); err != nil {
logger.Errorf("error generating preview: %s", err.Error()) logger.Errorf("error generating preview: %s", err.Error())

View file

@ -1,21 +1,23 @@
package manager package manager
import ( import (
"sync"
"github.com/stashapp/stash/pkg/ffmpeg" "github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils" "github.com/stashapp/stash/pkg/utils"
"sync"
) )
type GenerateSpriteTask struct { type GenerateSpriteTask struct {
Scene models.Scene Scene models.Scene
Overwrite bool
} }
func (t *GenerateSpriteTask) Start(wg *sync.WaitGroup) { func (t *GenerateSpriteTask) Start(wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
if t.doesSpriteExist(t.Scene.Checksum) { if t.doesSpriteExist(t.Scene.Checksum) && !t.Overwrite {
return return
} }
@ -28,6 +30,7 @@ func (t *GenerateSpriteTask) Start(wg *sync.WaitGroup) {
imagePath := instance.Paths.Scene.GetSpriteImageFilePath(t.Scene.Checksum) imagePath := instance.Paths.Scene.GetSpriteImageFilePath(t.Scene.Checksum)
vttPath := instance.Paths.Scene.GetSpriteVttFilePath(t.Scene.Checksum) vttPath := instance.Paths.Scene.GetSpriteVttFilePath(t.Scene.Checksum)
generator, err := NewSpriteGenerator(*videoFile, imagePath, vttPath, 9, 9) generator, err := NewSpriteGenerator(*videoFile, imagePath, vttPath, 9, 9)
generator.Overwrite = t.Overwrite
if err != nil { if err != nil {
logger.Errorf("error creating sprite generator: %s", err.Error()) logger.Errorf("error creating sprite generator: %s", err.Error())
return return

View file

@ -12,13 +12,14 @@ import (
type GenerateTranscodeTask struct { type GenerateTranscodeTask struct {
Scene models.Scene Scene models.Scene
Overwrite bool
} }
func (t *GenerateTranscodeTask) Start(wg *sync.WaitGroup) { func (t *GenerateTranscodeTask) Start(wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
hasTranscode, _ := HasTranscode(&t.Scene) hasTranscode, _ := HasTranscode(&t.Scene)
if hasTranscode { if !t.Overwrite && hasTranscode {
return return
} }
@ -107,7 +108,7 @@ func (t *GenerateTranscodeTask) isTranscodeNeeded() bool {
} }
hasTranscode, _ := HasTranscode(&t.Scene) hasTranscode, _ := HasTranscode(&t.Scene)
if hasTranscode { if !t.Overwrite && hasTranscode {
return false return false
} }
return true return true

View file

@ -2,6 +2,7 @@ package models
import ( import (
"database/sql" "database/sql"
"fmt"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -82,6 +83,24 @@ func (qb *GalleryQueryBuilder) Find(id int) (*Gallery, error) {
return qb.queryGallery(query, args, nil) return qb.queryGallery(query, args, nil)
} }
func (qb *GalleryQueryBuilder) FindMany(ids []int) ([]*Gallery, error) {
var galleries []*Gallery
for _, id := range ids {
gallery, err := qb.Find(id)
if err != nil {
return nil, err
}
if gallery == nil {
return nil, fmt.Errorf("gallery with id %d not found", id)
}
galleries = append(galleries, gallery)
}
return galleries, nil
}
func (qb *GalleryQueryBuilder) FindByChecksum(checksum string, tx *sqlx.Tx) (*Gallery, error) { func (qb *GalleryQueryBuilder) FindByChecksum(checksum string, tx *sqlx.Tx) (*Gallery, error) {
query := "SELECT * FROM galleries WHERE checksum = ? LIMIT 1" query := "SELECT * FROM galleries WHERE checksum = ? LIMIT 1"
args := []interface{}{checksum} args := []interface{}{checksum}

View file

@ -2,6 +2,7 @@ package models
import ( import (
"database/sql" "database/sql"
"fmt"
"strings" "strings"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -147,6 +148,24 @@ func (qb *SceneQueryBuilder) Find(id int) (*Scene, error) {
return qb.find(id, nil) return qb.find(id, nil)
} }
func (qb *SceneQueryBuilder) FindMany(ids []int) ([]*Scene, error) {
var scenes []*Scene
for _, id := range ids {
scene, err := qb.Find(id)
if err != nil {
return nil, err
}
if scene == nil {
return nil, fmt.Errorf("scene with id %d not found", id)
}
scenes = append(scenes, scene)
}
return scenes, nil
}
func (qb *SceneQueryBuilder) find(id int, tx *sqlx.Tx) (*Scene, error) { func (qb *SceneQueryBuilder) find(id int, tx *sqlx.Tx) (*Scene, error) {
query := selectAll(sceneTable) + "WHERE id = ? LIMIT 1" query := selectAll(sceneTable) + "WHERE id = ? LIMIT 1"
args := []interface{}{id} args := []interface{}{id}

View file

@ -2,9 +2,11 @@ package models
import ( import (
"database/sql" "database/sql"
"fmt"
"strconv"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database" "github.com/stashapp/stash/pkg/database"
"strconv"
) )
const countSceneMarkersForTagQuery = ` const countSceneMarkersForTagQuery = `
@ -72,6 +74,24 @@ func (qb *SceneMarkerQueryBuilder) Find(id int) (*SceneMarker, error) {
return results[0], nil return results[0], nil
} }
func (qb *SceneMarkerQueryBuilder) FindMany(ids []int) ([]*SceneMarker, error) {
var markers []*SceneMarker
for _, id := range ids {
marker, err := qb.Find(id)
if err != nil {
return nil, err
}
if marker == nil {
return nil, fmt.Errorf("scene marker with id %d not found", id)
}
markers = append(markers, marker)
}
return markers, nil
}
func (qb *SceneMarkerQueryBuilder) FindBySceneID(sceneID int, tx *sqlx.Tx) ([]*SceneMarker, error) { func (qb *SceneMarkerQueryBuilder) FindBySceneID(sceneID int, tx *sqlx.Tx) ([]*SceneMarker, error) {
query := ` query := `
SELECT scene_markers.* FROM scene_markers SELECT scene_markers.* FROM scene_markers

View file

@ -1,5 +1,7 @@
package utils package utils
import "strconv"
// https://gobyexample.com/collection-functions // https://gobyexample.com/collection-functions
func StrIndex(vs []string, t string) int { func StrIndex(vs []string, t string) int {
@ -32,3 +34,15 @@ func StrMap(vs []string, f func(string) string) []string {
} }
return vsm return vsm
} }
// StringSliceToIntSlice converts a slice of strings to a slice of ints. If any
// values cannot be parsed, then they are inserted into the returned slice as
// 0.
func StringSliceToIntSlice(ss []string) []int {
ret := make([]int, len(ss))
for i, v := range ss {
ret[i], _ = strconv.Atoi(v)
}
return ret
}

View file

@ -3,6 +3,7 @@ import ReactMarkdown from "react-markdown";
const markup = ` const markup = `
### New Features ### New Features
* Support (re-)generation of generated content for specific scenes.
* Add tag thumbnails, tags grid view and tag page. * Add tag thumbnails, tags grid view and tag page.
* Add post-scrape dialog. * Add post-scrape dialog.
* Add various keyboard shortcuts (see manual). * Add various keyboard shortcuts (see manual).

View file

@ -25,6 +25,7 @@ import { AddFilter } from "./AddFilter";
interface IListFilterOperation { interface IListFilterOperation {
text: string; text: string;
onClick: () => void; onClick: () => void;
isDisplayed?: () => boolean;
} }
interface IListFilterProps { interface IListFilterProps {
@ -363,7 +364,15 @@ export const ListFilter: React.FC<IListFilterProps> = (
const options = [renderSelectAll(), renderSelectNone()]; const options = [renderSelectAll(), renderSelectNone()];
if (props.otherOperations) { if (props.otherOperations) {
props.otherOperations.forEach((o) => { props.otherOperations
.filter((o) => {
if (!o.isDisplayed) {
return true;
}
return o.isDisplayed();
})
.forEach((o) => {
options.push( options.push(
<Dropdown.Item <Dropdown.Item
key={o.text} key={o.text}

View file

@ -23,6 +23,7 @@ import { SceneDetailPanel } from "./SceneDetailPanel";
import { OCounterButton } from "./OCounterButton"; import { OCounterButton } from "./OCounterButton";
import { SceneMoviePanel } from "./SceneMoviePanel"; import { SceneMoviePanel } from "./SceneMoviePanel";
import { DeleteScenesDialog } from "../DeleteScenesDialog"; import { DeleteScenesDialog } from "../DeleteScenesDialog";
import { SceneGenerateDialog } from "../SceneGenerateDialog";
export const Scene: React.FC = () => { export const Scene: React.FC = () => {
const { id = "new" } = useParams(); const { id = "new" } = useParams();
@ -42,6 +43,7 @@ export const Scene: React.FC = () => {
const [activeTabKey, setActiveTabKey] = useState("scene-details-panel"); const [activeTabKey, setActiveTabKey] = useState("scene-details-panel");
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
const queryParams = queryString.parse(location.search); const queryParams = queryString.parse(location.search);
const autoplay = queryParams?.autoplay === "true"; const autoplay = queryParams?.autoplay === "true";
@ -134,6 +136,19 @@ export const Scene: React.FC = () => {
} }
} }
function maybeRenderSceneGenerateDialog() {
if (isGenerateDialogOpen && scene) {
return (
<SceneGenerateDialog
selectedIds={[scene.id]}
onClose={() => {
setIsGenerateDialogOpen(false);
}}
/>
);
}
}
function renderOperations() { function renderOperations() {
return ( return (
<Dropdown> <Dropdown>
@ -146,6 +161,13 @@ export const Scene: React.FC = () => {
<Icon icon="ellipsis-v" /> <Icon icon="ellipsis-v" />
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu className="bg-secondary text-white"> <Dropdown.Menu className="bg-secondary text-white">
<Dropdown.Item
key="generate"
className="bg-secondary text-white"
onClick={() => setIsGenerateDialogOpen(true)}
>
Generate...
</Dropdown.Item>
<Dropdown.Item <Dropdown.Item
key="generate-screenshot" key="generate-screenshot"
className="bg-secondary text-white" className="bg-secondary text-white"
@ -291,6 +313,7 @@ export const Scene: React.FC = () => {
return ( return (
<div className="row"> <div className="row">
{maybeRenderSceneGenerateDialog()}
{maybeRenderDeleteDialog()} {maybeRenderDeleteDialog()}
<div className="scene-tabs order-xl-first order-last"> <div className="scene-tabs order-xl-first order-last">
<div className="d-none d-xl-block"> <div className="d-none d-xl-block">

View file

@ -0,0 +1,105 @@
import React, { useState } from "react";
import { Form } from "react-bootstrap";
import { mutateMetadataGenerate } from "src/core/StashService";
import { Modal } from "src/components/Shared";
import { useToast } from "src/hooks";
interface ISceneGenerateDialogProps {
selectedIds: string[];
onClose: () => void;
}
export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
props: ISceneGenerateDialogProps
) => {
const [sprites, setSprites] = useState(true);
const [previews, setPreviews] = useState(true);
const [markers, setMarkers] = useState(true);
const [transcodes, setTranscodes] = useState(false);
const [overwrite, setOverwrite] = useState(true);
const [imagePreviews, setImagePreviews] = useState(false);
const Toast = useToast();
async function onGenerate() {
try {
await mutateMetadataGenerate({
sprites,
previews,
imagePreviews: previews && imagePreviews,
markers,
transcodes,
thumbnails: false,
overwrite,
sceneIDs: props.selectedIds,
});
Toast.success({ content: "Started generating" });
} catch (e) {
Toast.error(e);
} finally {
props.onClose();
}
}
return (
<Modal
show
icon="cogs"
header="Generate"
accept={{ onClick: onGenerate, text: "Generate" }}
cancel={{
onClick: () => props.onClose(),
text: "Cancel",
variant: "secondary",
}}
>
<Form>
<Form.Group>
<Form.Check
id="preview-task"
checked={previews}
label="Previews (video previews which play when hovering over a scene)"
onChange={() => setPreviews(!previews)}
/>
<div className="d-flex flex-row">
<div></div>
<Form.Check
id="image-preview-task"
checked={imagePreviews}
disabled={!previews}
label="Image Previews (animated WebP previews, only required if Preview Type is set to Animated Image)"
onChange={() => setImagePreviews(!imagePreviews)}
className="ml-2 flex-grow"
/>
</div>
<Form.Check
id="sprite-task"
checked={sprites}
label="Sprites (for the scene scrubber)"
onChange={() => setSprites(!sprites)}
/>
<Form.Check
id="marker-task"
checked={markers}
label="Markers (20 second videos which begin at the given timecode)"
onChange={() => setMarkers(!markers)}
/>
<Form.Check
id="transcode-task"
checked={transcodes}
label="Transcodes (MP4 conversions of unsupported video formats)"
onChange={() => setTranscodes(!transcodes)}
/>
<hr />
<Form.Check
id="overwrite"
checked={overwrite}
label="Overwrite existing generated files"
onChange={() => setOverwrite(!overwrite)}
/>
</Form.Group>
</Form>
</Modal>
);
};

View file

@ -1,4 +1,4 @@
import React from "react"; import React, { useState } from "react";
import _ from "lodash"; import _ from "lodash";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { import {
@ -9,11 +9,13 @@ import { queryFindScenes } from "src/core/StashService";
import { useScenesList } from "src/hooks"; import { useScenesList } from "src/hooks";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types"; import { DisplayMode } from "src/models/list-filter/types";
import { showWhenSelected } from "src/hooks/ListHook";
import { WallPanel } from "../Wall/WallPanel"; import { WallPanel } from "../Wall/WallPanel";
import { SceneCard } from "./SceneCard"; import { SceneCard } from "./SceneCard";
import { SceneListTable } from "./SceneListTable"; import { SceneListTable } from "./SceneListTable";
import { EditScenesDialog } from "./EditScenesDialog"; import { EditScenesDialog } from "./EditScenesDialog";
import { DeleteScenesDialog } from "./DeleteScenesDialog"; import { DeleteScenesDialog } from "./DeleteScenesDialog";
import { SceneGenerateDialog } from "./SceneGenerateDialog";
interface ISceneList { interface ISceneList {
subComponent?: boolean; subComponent?: boolean;
@ -25,11 +27,18 @@ export const SceneList: React.FC<ISceneList> = ({
filterHook, filterHook,
}) => { }) => {
const history = useHistory(); const history = useHistory();
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
const otherOperations = [ const otherOperations = [
{ {
text: "Play Random", text: "Play Random",
onClick: playRandom, onClick: playRandom,
}, },
{
text: "Generate...",
onClick: generate,
isDisplayed: showWhenSelected,
},
]; ];
const addKeybinds = ( const addKeybinds = (
@ -82,6 +91,25 @@ export const SceneList: React.FC<ISceneList> = ({
} }
} }
async function generate() {
setIsGenerateDialogOpen(true);
}
function maybeRenderSceneGenerateDialog(selectedIds: Set<string>) {
if (isGenerateDialogOpen) {
return (
<>
<SceneGenerateDialog
selectedIds={Array.from(selectedIds.values())}
onClose={() => {
setIsGenerateDialogOpen(false);
}}
/>
</>
);
}
}
function renderEditScenesDialog( function renderEditScenesDialog(
selectedScenes: SlimSceneDataFragment[], selectedScenes: SlimSceneDataFragment[],
onClose: (applied: boolean) => void onClose: (applied: boolean) => void
@ -123,7 +151,7 @@ export const SceneList: React.FC<ISceneList> = ({
); );
} }
function renderContent( function renderScenes(
result: FindScenesQueryResult, result: FindScenesQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string>, selectedIds: Set<string>,
@ -149,5 +177,19 @@ export const SceneList: React.FC<ISceneList> = ({
} }
} }
function renderContent(
result: FindScenesQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
zoomIndex: number
) {
return (
<>
{maybeRenderSceneGenerateDialog(selectedIds)}
{renderScenes(result, filter, selectedIds, zoomIndex)}
</>
);
}
return listData.template; return listData.template;
}; };

View file

@ -17,6 +17,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
undefined undefined
); );
const [cachePath, setCachePath] = useState<string | undefined>(undefined); const [cachePath, setCachePath] = useState<string | undefined>(undefined);
const [previewPreset, setPreviewPreset] = useState<string>(
GQL.PreviewPreset.Slow
);
const [maxTranscodeSize, setMaxTranscodeSize] = useState< const [maxTranscodeSize, setMaxTranscodeSize] = useState<
GQL.StreamingResolutionEnum | undefined GQL.StreamingResolutionEnum | undefined
>(undefined); >(undefined);
@ -44,6 +47,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
databasePath, databasePath,
generatedPath, generatedPath,
cachePath, cachePath,
previewPreset: (previewPreset as GQL.PreviewPreset) ?? undefined,
maxTranscodeSize, maxTranscodeSize,
maxStreamingTranscodeSize, maxStreamingTranscodeSize,
forceMkv, forceMkv,
@ -68,6 +72,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
setDatabasePath(conf.general.databasePath); setDatabasePath(conf.general.databasePath);
setGeneratedPath(conf.general.generatedPath); setGeneratedPath(conf.general.generatedPath);
setCachePath(conf.general.cachePath); setCachePath(conf.general.cachePath);
setPreviewPreset(conf.general.previewPreset);
setMaxTranscodeSize(conf.general.maxTranscodeSize ?? undefined); setMaxTranscodeSize(conf.general.maxTranscodeSize ?? undefined);
setMaxStreamingTranscodeSize( setMaxStreamingTranscodeSize(
conf.general.maxStreamingTranscodeSize ?? undefined conf.general.maxStreamingTranscodeSize ?? undefined
@ -274,10 +279,32 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Group> <Form.Group>
<h4>Video</h4> <h4>Video</h4>
<Form.Group id="transcode-size">
<h6>Preview encoding preset</h6>
<Form.Control
className="w-auto input-control"
as="select"
value={previewPreset}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
setPreviewPreset(e.currentTarget.value)
}
>
{Object.keys(GQL.PreviewPreset).map((p) => (
<option value={p.toLowerCase()} key={p}>
{p}
</option>
))}
</Form.Control>
<Form.Text className="text-muted">
The preset regulates size, quality and encoding time of preview
generation. Presets beyond slow have diminishing returns and are
not recommended.
</Form.Text>
</Form.Group>
<Form.Group id="transcode-size"> <Form.Group id="transcode-size">
<h6>Maximum transcode size</h6> <h6>Maximum transcode size</h6>
<Form.Control <Form.Control
className="col col-sm-6 input-control" className="w-auto input-control"
as="select" as="select"
onChange={(event: React.ChangeEvent<HTMLSelectElement>) => onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
setMaxTranscodeSize(translateQuality(event.currentTarget.value)) setMaxTranscodeSize(translateQuality(event.currentTarget.value))
@ -297,7 +324,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Group id="streaming-transcode-size"> <Form.Group id="streaming-transcode-size">
<h6>Maximum streaming transcode size</h6> <h6>Maximum streaming transcode size</h6>
<Form.Control <Form.Control
className="col col-sm-6 input-control" className="w-auto input-control"
as="select" as="select"
onChange={(event: React.ChangeEvent<HTMLSelectElement>) => onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
setMaxStreamingTranscodeSize( setMaxStreamingTranscodeSize(

View file

@ -1,7 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Button, Form } from "react-bootstrap"; import { Button, Form } from "react-bootstrap";
import { mutateMetadataGenerate } from "src/core/StashService"; import { mutateMetadataGenerate } from "src/core/StashService";
import { PreviewPreset } from "src/core/generated-graphql";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
export const GenerateButton: React.FC = () => { export const GenerateButton: React.FC = () => {
@ -12,9 +11,6 @@ export const GenerateButton: React.FC = () => {
const [transcodes, setTranscodes] = useState(false); const [transcodes, setTranscodes] = useState(false);
const [thumbnails, setThumbnails] = useState(false); const [thumbnails, setThumbnails] = useState(false);
const [imagePreviews, setImagePreviews] = useState(false); const [imagePreviews, setImagePreviews] = useState(false);
const [previewPreset, setPreviewPreset] = useState<string>(
PreviewPreset.Slow
);
async function onGenerate() { async function onGenerate() {
try { try {
@ -25,7 +21,6 @@ export const GenerateButton: React.FC = () => {
markers, markers,
transcodes, transcodes,
thumbnails, thumbnails,
previewPreset: (previewPreset as PreviewPreset) ?? undefined,
}); });
Toast.success({ content: "Started generating" }); Toast.success({ content: "Started generating" });
} catch (e) { } catch (e) {
@ -53,31 +48,6 @@ export const GenerateButton: React.FC = () => {
className="ml-2 flex-grow" className="ml-2 flex-grow"
/> />
</div> </div>
<Form.Group controlId="preview-preset" className="mt-2">
<Form.Label>
<h6>Preview encoding preset</h6>
</Form.Label>
<Form.Control
as="select"
value={previewPreset}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
setPreviewPreset(e.currentTarget.value)
}
disabled={!previews}
className="col-1"
>
{Object.keys(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.Check <Form.Check
id="sprite-task" id="sprite-task"
checked={sprites} checked={sprites}

View file

@ -51,6 +51,11 @@ interface IListHookOperation<T> {
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string> selectedIds: Set<string>
) => void; ) => void;
isDisplayed?: (
result: T,
filter: ListFilterModel,
selectedIds: Set<string>
) => boolean;
} }
interface IListHookOptions<T, E> { interface IListHookOptions<T, E> {
@ -346,6 +351,13 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
onClick: () => { onClick: () => {
o.onClick(result, filter, selectedIds); o.onClick(result, filter, selectedIds);
}, },
isDisplayed: () => {
if (o.isDisplayed) {
return o.isDisplayed(result, filter, selectedIds);
}
return true;
},
}; };
}) })
: undefined; : undefined;
@ -594,3 +606,11 @@ export const useTagsList = (
getSelectedData: (result: FindTagsQueryResult, selectedIds: Set<string>) => getSelectedData: (result: FindTagsQueryResult, selectedIds: Set<string>) =>
getSelectedData(result?.data?.findTags?.tags ?? [], selectedIds), getSelectedData(result?.data?.findTags?.tags ?? [], selectedIds),
}); });
export const showWhenSelected = (
result: FindScenesQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>
) => {
return selectedIds.size > 0;
};