mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Generate cover image (#376)
* Make mutating metadata ops mutation * Implement scene generate screenshot * Remove fetch policy on metadata mutations * Port UI changes to v2.5 * Set generated image in database
This commit is contained in:
53
pkg/api/resolver_mutation_metadata.go
Normal file
53
pkg/api/resolver_mutation_metadata.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
|
||||
manager.GetInstance().Scan(input.UseFileMetadata)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataImport(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Import()
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataExport(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Export()
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.GenerateMetadataInput) (string, error) {
|
||||
manager.GetInstance().Generate(input.Sprites, input.Previews, input.Markers, input.Transcodes)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataAutoTag(ctx context.Context, input models.AutoTagMetadataInput) (string, error) {
|
||||
manager.GetInstance().AutoTag(input.Performers, input.Studios, input.Tags)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataClean(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Clean()
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) JobStatus(ctx context.Context) (*models.MetadataUpdateStatus, error) {
|
||||
status := manager.GetInstance().Status
|
||||
ret := models.MetadataUpdateStatus{
|
||||
Progress: status.Progress,
|
||||
Status: status.Status.String(),
|
||||
Message: "",
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StopJob(ctx context.Context) (bool, error) {
|
||||
return manager.GetInstance().Status.Stop(), nil
|
||||
}
|
||||
@@ -500,3 +500,13 @@ func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (int, err
|
||||
|
||||
return newVal, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneGenerateScreenshot(ctx context.Context, id string, at *float64) (string, error) {
|
||||
if at != nil {
|
||||
manager.GetInstance().GenerateScreenshot(id, *at)
|
||||
} else {
|
||||
manager.GetInstance().GenerateDefaultScreenshot(id)
|
||||
}
|
||||
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
@@ -7,36 +7,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *queryResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
|
||||
manager.GetInstance().Scan(input.UseFileMetadata)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) MetadataImport(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Import()
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) MetadataExport(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Export()
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) MetadataGenerate(ctx context.Context, input models.GenerateMetadataInput) (string, error) {
|
||||
manager.GetInstance().Generate(input.Sprites, input.Previews, input.Markers, input.Transcodes)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) MetadataAutoTag(ctx context.Context, input models.AutoTagMetadataInput) (string, error) {
|
||||
manager.GetInstance().AutoTag(input.Performers, input.Studios, input.Tags)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) MetadataClean(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Clean()
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) JobStatus(ctx context.Context) (*models.MetadataUpdateStatus, error) {
|
||||
status := manager.GetInstance().Status
|
||||
ret := models.MetadataUpdateStatus{
|
||||
@@ -47,7 +17,3 @@ func (r *queryResolver) JobStatus(ctx context.Context) (*models.MetadataUpdateSt
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) StopJob(ctx context.Context) (bool, error) {
|
||||
return manager.GetInstance().Status.Stop(), nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ type ScreenshotOptions struct {
|
||||
Verbosity string
|
||||
}
|
||||
|
||||
func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) {
|
||||
func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) error {
|
||||
if options.Verbosity == "" {
|
||||
options.Verbosity = "error"
|
||||
}
|
||||
@@ -28,5 +28,7 @@ func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) {
|
||||
"-f", "image2",
|
||||
options.OutputPath,
|
||||
}
|
||||
_, _ = e.run(probeResult, args)
|
||||
_, err := e.run(probeResult, args)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -216,6 +216,55 @@ func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcod
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *singleton) GenerateDefaultScreenshot(sceneId string) {
|
||||
s.generateScreenshot(sceneId, nil)
|
||||
}
|
||||
|
||||
func (s *singleton) GenerateScreenshot(sceneId string, at float64) {
|
||||
s.generateScreenshot(sceneId, &at)
|
||||
}
|
||||
|
||||
// generate default screenshot if at is nil
|
||||
func (s *singleton) generateScreenshot(sceneId string, at *float64) {
|
||||
if s.Status.Status != Idle {
|
||||
return
|
||||
}
|
||||
s.Status.SetStatus(Generate)
|
||||
s.Status.indefiniteProgress()
|
||||
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
instance.Paths.Generated.EnsureTmpDir()
|
||||
|
||||
go func() {
|
||||
defer s.returnToIdleState()
|
||||
|
||||
sceneIdInt, err := strconv.Atoi(sceneId)
|
||||
if err != nil {
|
||||
logger.Errorf("Error parsing scene id %s: %s", sceneId, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
scene, err := qb.Find(sceneIdInt)
|
||||
if err != nil || scene == nil {
|
||||
logger.Errorf("failed to get scene for generate")
|
||||
return
|
||||
}
|
||||
|
||||
task := GenerateScreenshotTask{
|
||||
Scene: *scene,
|
||||
ScreenshotAt: at,
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go task.Start(&wg)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
logger.Infof("Generate finished")
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *singleton) AutoTag(performerIds []string, studioIds []string, tagIds []string) {
|
||||
if s.Status.Status != Idle {
|
||||
return
|
||||
|
||||
16
pkg/manager/screenshot.go
Normal file
16
pkg/manager/screenshot.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
)
|
||||
|
||||
func makeScreenshot(probeResult ffmpeg.VideoFile, outputPath string, quality int, width int, time float64) {
|
||||
encoder := ffmpeg.NewEncoder(instance.FFMPEGPath)
|
||||
options := ffmpeg.ScreenshotOptions{
|
||||
OutputPath: outputPath,
|
||||
Quality: quality,
|
||||
Time: time,
|
||||
Width: width,
|
||||
}
|
||||
encoder.Screenshot(probeResult, options)
|
||||
}
|
||||
84
pkg/manager/task_generate_screenshot.go
Normal file
84
pkg/manager/task_generate_screenshot.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type GenerateScreenshotTask struct {
|
||||
Scene models.Scene
|
||||
ScreenshotAt *float64
|
||||
}
|
||||
|
||||
func (t *GenerateScreenshotTask) Start(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
scenePath := t.Scene.Path
|
||||
probeResult, err := ffmpeg.NewVideoFile(instance.FFProbePath, scenePath)
|
||||
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var at float64
|
||||
if t.ScreenshotAt == nil {
|
||||
at = float64(probeResult.Duration) * 0.2
|
||||
} else {
|
||||
at = *t.ScreenshotAt
|
||||
}
|
||||
|
||||
checksum := t.Scene.Checksum
|
||||
normalPath := instance.Paths.Scene.GetScreenshotPath(checksum)
|
||||
|
||||
// we'll generate the screenshot, grab the generated data and set it
|
||||
// in the database. We'll use SetSceneScreenshot to set the data
|
||||
// which also generates the thumbnail
|
||||
|
||||
logger.Debugf("Creating screenshot for %s", scenePath)
|
||||
makeScreenshot(*probeResult, normalPath, 2, probeResult.Width, at)
|
||||
|
||||
f, err := os.Open(normalPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Error reading screenshot: %s", err.Error())
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
coverImageData, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
logger.Errorf("Error reading screenshot: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
updatedTime := time.Now()
|
||||
updatedScene := models.ScenePartial{
|
||||
ID: t.Scene.ID,
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
|
||||
updatedScene.Cover = &coverImageData
|
||||
err = SetSceneScreenshot(t.Scene.Checksum, coverImageData)
|
||||
_, err = qb.Update(updatedScene, tx)
|
||||
if err != nil {
|
||||
logger.Errorf("Error setting screenshot: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
logger.Errorf("Error setting screenshot: %s", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -177,28 +177,19 @@ func (t *ScanTask) makeScreenshots(probeResult *ffmpeg.VideoFile, checksum strin
|
||||
logger.Infof("Regenerating images for %s", t.FilePath)
|
||||
}
|
||||
|
||||
at := float64(probeResult.Duration) * 0.2
|
||||
|
||||
if !thumbExists {
|
||||
logger.Debugf("Creating thumbnail for %s", t.FilePath)
|
||||
t.makeScreenshot(*probeResult, thumbPath, 5, 320)
|
||||
makeScreenshot(*probeResult, thumbPath, 5, 320, at)
|
||||
}
|
||||
|
||||
if !normalExists {
|
||||
logger.Debugf("Creating screenshot for %s", t.FilePath)
|
||||
t.makeScreenshot(*probeResult, normalPath, 2, probeResult.Width)
|
||||
makeScreenshot(*probeResult, normalPath, 2, probeResult.Width, at)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ScanTask) makeScreenshot(probeResult ffmpeg.VideoFile, outputPath string, quality int, width int) {
|
||||
encoder := ffmpeg.NewEncoder(instance.FFMPEGPath)
|
||||
options := ffmpeg.ScreenshotOptions{
|
||||
OutputPath: outputPath,
|
||||
Quality: quality,
|
||||
Time: float64(probeResult.Duration) * 0.2,
|
||||
Width: width,
|
||||
}
|
||||
encoder.Screenshot(probeResult, options)
|
||||
}
|
||||
|
||||
func (t *ScanTask) calculateChecksum() (string, error) {
|
||||
logger.Infof("%s not found. Calculating checksum...", t.FilePath)
|
||||
checksum, err := utils.MD5FromFilePath(t.FilePath)
|
||||
|
||||
Reference in New Issue
Block a user