mirror of https://github.com/stashapp/stash.git
197 lines
5.2 KiB
Go
197 lines
5.2 KiB
Go
package scene
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/stashapp/stash/pkg/fsutil"
|
|
"github.com/stashapp/stash/pkg/logger"
|
|
"github.com/stashapp/stash/pkg/models"
|
|
"github.com/stashapp/stash/pkg/sliceutil"
|
|
"github.com/stashapp/stash/pkg/txn"
|
|
)
|
|
|
|
type MergeOptions struct {
|
|
ScenePartial models.ScenePartial
|
|
IncludePlayHistory bool
|
|
IncludeOHistory bool
|
|
}
|
|
|
|
func (s *Service) Merge(ctx context.Context, sourceIDs []int, destinationID int, fileDeleter *FileDeleter, options MergeOptions) error {
|
|
scenePartial := options.ScenePartial
|
|
|
|
// ensure source ids are unique
|
|
sourceIDs = sliceutil.AppendUniques(nil, sourceIDs)
|
|
|
|
// ensure destination is not in source list
|
|
if slices.Contains(sourceIDs, destinationID) {
|
|
return errors.New("destination scene cannot be in source list")
|
|
}
|
|
|
|
dest, err := s.Repository.Find(ctx, destinationID)
|
|
if err != nil {
|
|
return fmt.Errorf("finding destination scene ID %d: %w", destinationID, err)
|
|
}
|
|
|
|
sources, err := s.Repository.FindMany(ctx, sourceIDs)
|
|
if err != nil {
|
|
return fmt.Errorf("finding source scenes: %w", err)
|
|
}
|
|
|
|
var fileIDs []models.FileID
|
|
|
|
for _, src := range sources {
|
|
if err := src.LoadRelationships(ctx, s.Repository); err != nil {
|
|
return fmt.Errorf("loading scene relationships from %d: %w", src.ID, err)
|
|
}
|
|
|
|
for _, f := range src.Files.List() {
|
|
fileIDs = append(fileIDs, f.Base().ID)
|
|
}
|
|
|
|
if err := s.mergeSceneMarkers(ctx, dest, src); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// move files to destination scene
|
|
if len(fileIDs) > 0 {
|
|
if err := s.Repository.AssignFiles(ctx, destinationID, fileIDs); err != nil {
|
|
return fmt.Errorf("moving files to destination scene: %w", err)
|
|
}
|
|
|
|
// if scene didn't already have a primary file, then set it now
|
|
if dest.PrimaryFileID == nil {
|
|
scenePartial.PrimaryFileID = &fileIDs[0]
|
|
} else {
|
|
// don't allow changing primary file ID from the input values
|
|
scenePartial.PrimaryFileID = nil
|
|
}
|
|
}
|
|
|
|
if _, err := s.Repository.UpdatePartial(ctx, destinationID, scenePartial); err != nil {
|
|
return fmt.Errorf("updating scene: %w", err)
|
|
}
|
|
|
|
// merge play history
|
|
if options.IncludePlayHistory {
|
|
var allDates []time.Time
|
|
for _, src := range sources {
|
|
thisDates, err := s.Repository.GetViewDates(ctx, src.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("getting view dates for scene %d: %w", src.ID, err)
|
|
}
|
|
|
|
allDates = append(allDates, thisDates...)
|
|
}
|
|
|
|
if len(allDates) > 0 {
|
|
if _, err := s.Repository.AddViews(ctx, destinationID, allDates); err != nil {
|
|
return fmt.Errorf("adding view dates to scene %d: %w", destinationID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// merge o history
|
|
if options.IncludeOHistory {
|
|
var allDates []time.Time
|
|
for _, src := range sources {
|
|
thisDates, err := s.Repository.GetODates(ctx, src.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("getting o dates for scene %d: %w", src.ID, err)
|
|
}
|
|
|
|
allDates = append(allDates, thisDates...)
|
|
}
|
|
|
|
if len(allDates) > 0 {
|
|
if _, err := s.Repository.AddO(ctx, destinationID, allDates); err != nil {
|
|
return fmt.Errorf("adding o dates to scene %d: %w", destinationID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// delete old scenes
|
|
for _, src := range sources {
|
|
const deleteGenerated = true
|
|
const deleteFile = false
|
|
if err := s.Destroy(ctx, src, fileDeleter, deleteGenerated, deleteFile); err != nil {
|
|
return fmt.Errorf("deleting scene %d: %w", src.ID, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) mergeSceneMarkers(ctx context.Context, dest *models.Scene, src *models.Scene) error {
|
|
markers, err := s.MarkerRepository.FindBySceneID(ctx, src.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("finding scene markers: %w", err)
|
|
}
|
|
|
|
type rename struct {
|
|
src string
|
|
dest string
|
|
}
|
|
|
|
var toRename []rename
|
|
|
|
destHash := dest.GetHash(s.Config.GetVideoFileNamingAlgorithm())
|
|
|
|
for _, m := range markers {
|
|
srcHash := src.GetHash(s.Config.GetVideoFileNamingAlgorithm())
|
|
|
|
// updated the scene id
|
|
m.SceneID = dest.ID
|
|
|
|
if err := s.MarkerRepository.Update(ctx, m); err != nil {
|
|
return fmt.Errorf("updating scene marker %d: %w", m.ID, err)
|
|
}
|
|
|
|
// move generated files to new location
|
|
toRename = append(toRename, []rename{
|
|
{
|
|
src: s.Paths.SceneMarkers.GetScreenshotPath(srcHash, int(m.Seconds)),
|
|
dest: s.Paths.SceneMarkers.GetScreenshotPath(destHash, int(m.Seconds)),
|
|
},
|
|
{
|
|
src: s.Paths.SceneMarkers.GetThumbnailPath(srcHash, int(m.Seconds)),
|
|
dest: s.Paths.SceneMarkers.GetThumbnailPath(destHash, int(m.Seconds)),
|
|
},
|
|
{
|
|
src: s.Paths.SceneMarkers.GetWebpPreviewPath(srcHash, int(m.Seconds)),
|
|
dest: s.Paths.SceneMarkers.GetWebpPreviewPath(destHash, int(m.Seconds)),
|
|
},
|
|
}...)
|
|
}
|
|
|
|
if len(toRename) > 0 {
|
|
txn.AddPostCommitHook(ctx, func(ctx context.Context) {
|
|
// rename the files if they exist
|
|
for _, e := range toRename {
|
|
srcExists, _ := fsutil.FileExists(e.src)
|
|
destExists, _ := fsutil.FileExists(e.dest)
|
|
|
|
if srcExists && !destExists {
|
|
destDir := filepath.Dir(e.dest)
|
|
if err := fsutil.EnsureDir(destDir); err != nil {
|
|
logger.Errorf("Error creating generated marker folder %s: %v", destDir, err)
|
|
continue
|
|
}
|
|
|
|
if err := os.Rename(e.src, e.dest); err != nil {
|
|
logger.Errorf("Error renaming generated marker file from %s to %s: %v", e.src, e.dest, err)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|