mirror of
https://github.com/stashapp/stash.git
synced 2025-12-07 00:43:12 +01:00
Generate content for specific scenes (#672)
* Add UI dialog for scene(s) * Move preview preset to config
This commit is contained in:
parent
8e4945325d
commit
c104c6d075
27 changed files with 552 additions and 148 deletions
|
|
@ -3,6 +3,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||||
databasePath
|
databasePath
|
||||||
generatedPath
|
generatedPath
|
||||||
cachePath
|
cachePath
|
||||||
|
previewPreset
|
||||||
maxTranscodeSize
|
maxTranscodeSize
|
||||||
maxStreamingTranscodeSize
|
maxStreamingTranscodeSize
|
||||||
forceMkv
|
forceMkv
|
||||||
|
|
|
||||||
|
|
@ -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"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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++
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -74,7 +106,6 @@ func (t *GenerateMarkersTask) Start(wg *sync.WaitGroup) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
105
ui/v2.5/src/components/Scenes/SceneGenerateDialog.tsx
Normal file
105
ui/v2.5/src/components/Scenes/SceneGenerateDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue