mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
* Markers can have end time Other metadata sources such as ThePornDB and timestamp.trade support end times for markers but Stash did not yet support saving those. This is a first step which only allows end time to be set either via API or via UI. Other aspects of Stash such as video player timeline are not yet updated to take end time into account. - User can set end time when creating or editing markers in the UI or in the API. - End time cannot be before start time. This is validated in the backend and for better UX also in the frontend. - End time is shown in scene details view or markers wall view if present. - GraphQL API does not require end_seconds. --------- Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
1128 lines
30 KiB
Go
1128 lines
30 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/stashapp/stash/internal/manager"
|
|
"github.com/stashapp/stash/pkg/file"
|
|
"github.com/stashapp/stash/pkg/logger"
|
|
"github.com/stashapp/stash/pkg/models"
|
|
"github.com/stashapp/stash/pkg/plugin"
|
|
"github.com/stashapp/stash/pkg/plugin/hook"
|
|
"github.com/stashapp/stash/pkg/scene"
|
|
"github.com/stashapp/stash/pkg/sliceutil"
|
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
|
"github.com/stashapp/stash/pkg/utils"
|
|
)
|
|
|
|
// used to refetch scene after hooks run
|
|
func (r *mutationResolver) getScene(ctx context.Context, id int) (ret *models.Scene, err error) {
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
ret, err = r.repository.Scene.Find(ctx, id)
|
|
return err
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCreateInput) (ret *models.Scene, err error) {
|
|
translator := changesetTranslator{
|
|
inputMap: getUpdateInputMap(ctx),
|
|
}
|
|
|
|
fileIDs, err := translator.fileIDSliceFromStringSlice(input.FileIds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting file ids: %w", err)
|
|
}
|
|
|
|
// Populate a new scene from the input
|
|
newScene := models.NewScene()
|
|
|
|
newScene.Title = translator.string(input.Title)
|
|
newScene.Code = translator.string(input.Code)
|
|
newScene.Details = translator.string(input.Details)
|
|
newScene.Director = translator.string(input.Director)
|
|
newScene.Rating = input.Rating100
|
|
newScene.Organized = translator.bool(input.Organized)
|
|
newScene.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs())
|
|
|
|
newScene.Date, err = translator.datePtr(input.Date)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting date: %w", err)
|
|
}
|
|
newScene.StudioID, err = translator.intPtrFromString(input.StudioID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting studio id: %w", err)
|
|
}
|
|
|
|
if input.Urls != nil {
|
|
newScene.URLs = models.NewRelatedStrings(input.Urls)
|
|
} else if input.URL != nil {
|
|
newScene.URLs = models.NewRelatedStrings([]string{*input.URL})
|
|
}
|
|
|
|
newScene.PerformerIDs, err = translator.relatedIds(input.PerformerIds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting performer ids: %w", err)
|
|
}
|
|
newScene.TagIDs, err = translator.relatedIds(input.TagIds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting tag ids: %w", err)
|
|
}
|
|
newScene.GalleryIDs, err = translator.relatedIds(input.GalleryIds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
|
}
|
|
|
|
// prefer groups over movies
|
|
if len(input.Groups) > 0 {
|
|
newScene.Groups, err = translator.relatedGroups(input.Groups)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting groups: %w", err)
|
|
}
|
|
} else if len(input.Movies) > 0 {
|
|
newScene.Groups, err = translator.relatedGroupsFromMovies(input.Movies)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting movies: %w", err)
|
|
}
|
|
}
|
|
|
|
var coverImageData []byte
|
|
if input.CoverImage != nil {
|
|
var err error
|
|
coverImageData, err = utils.ProcessImageInput(ctx, *input.CoverImage)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("processing cover image: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
ret, err = r.Resolver.sceneService.Create(ctx, &newScene, fileIDs, coverImageData)
|
|
return err
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUpdateInput) (ret *models.Scene, err error) {
|
|
translator := changesetTranslator{
|
|
inputMap: getUpdateInputMap(ctx),
|
|
}
|
|
|
|
// Start the transaction and save the scene
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
ret, err = r.sceneUpdate(ctx, input, translator)
|
|
return err
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, hook.SceneUpdatePost, input, translator.getFields())
|
|
return r.getScene(ctx, ret.ID)
|
|
}
|
|
|
|
func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.SceneUpdateInput) (ret []*models.Scene, err error) {
|
|
inputMaps := getUpdateInputMaps(ctx)
|
|
|
|
// Start the transaction and save the scenes
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
for i, scene := range input {
|
|
translator := changesetTranslator{
|
|
inputMap: inputMaps[i],
|
|
}
|
|
|
|
thisScene, err := r.sceneUpdate(ctx, *scene, translator)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ret = append(ret, thisScene)
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// execute post hooks outside of txn
|
|
var newRet []*models.Scene
|
|
for i, scene := range ret {
|
|
translator := changesetTranslator{
|
|
inputMap: inputMaps[i],
|
|
}
|
|
|
|
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, hook.SceneUpdatePost, input, translator.getFields())
|
|
|
|
scene, err = r.getScene(ctx, scene.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newRet = append(newRet, scene)
|
|
}
|
|
|
|
return newRet, nil
|
|
}
|
|
|
|
func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTranslator) (*models.ScenePartial, error) {
|
|
updatedScene := models.NewScenePartial()
|
|
|
|
updatedScene.Title = translator.optionalString(input.Title, "title")
|
|
updatedScene.Code = translator.optionalString(input.Code, "code")
|
|
updatedScene.Details = translator.optionalString(input.Details, "details")
|
|
updatedScene.Director = translator.optionalString(input.Director, "director")
|
|
updatedScene.Rating = translator.optionalInt(input.Rating100, "rating100")
|
|
|
|
if input.OCounter != nil {
|
|
logger.Warnf("o_counter is deprecated and no longer supported, use sceneIncrementO/sceneDecrementO instead")
|
|
}
|
|
|
|
if input.PlayCount != nil {
|
|
logger.Warnf("play_count is deprecated and no longer supported, use sceneIncrementPlayCount/sceneDecrementPlayCount instead")
|
|
}
|
|
|
|
updatedScene.PlayDuration = translator.optionalFloat64(input.PlayDuration, "play_duration")
|
|
updatedScene.Organized = translator.optionalBool(input.Organized, "organized")
|
|
updatedScene.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")
|
|
|
|
var err error
|
|
|
|
updatedScene.Date, err = translator.optionalDate(input.Date, "date")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting date: %w", err)
|
|
}
|
|
updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting studio id: %w", err)
|
|
}
|
|
|
|
updatedScene.URLs = translator.optionalURLs(input.Urls, input.URL)
|
|
|
|
updatedScene.PrimaryFileID, err = translator.fileIDPtrFromString(input.PrimaryFileID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting primary file id: %w", err)
|
|
}
|
|
|
|
updatedScene.PerformerIDs, err = translator.updateIds(input.PerformerIds, "performer_ids")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting performer ids: %w", err)
|
|
}
|
|
updatedScene.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting tag ids: %w", err)
|
|
}
|
|
updatedScene.GalleryIDs, err = translator.updateIds(input.GalleryIds, "gallery_ids")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
|
}
|
|
|
|
if translator.hasField("groups") {
|
|
updatedScene.GroupIDs, err = translator.updateGroupIDs(input.Groups, "groups")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting groups: %w", err)
|
|
}
|
|
} else if translator.hasField("movies") {
|
|
updatedScene.GroupIDs, err = translator.updateGroupIDsFromMovies(input.Movies, "movies")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting movies: %w", err)
|
|
}
|
|
}
|
|
|
|
return &updatedScene, nil
|
|
}
|
|
|
|
func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUpdateInput, translator changesetTranslator) (*models.Scene, error) {
|
|
sceneID, err := strconv.Atoi(input.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting id: %w", err)
|
|
}
|
|
|
|
qb := r.repository.Scene
|
|
|
|
originalScene, err := qb.Find(ctx, sceneID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if originalScene == nil {
|
|
return nil, fmt.Errorf("scene with id %d not found", sceneID)
|
|
}
|
|
|
|
// Populate scene from the input
|
|
updatedScene, err := scenePartialFromInput(input, translator)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// ensure that title is set where scene has no file
|
|
if updatedScene.Title.Set && updatedScene.Title.Value == "" {
|
|
if err := originalScene.LoadFiles(ctx, r.repository.Scene); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(originalScene.Files.List()) == 0 {
|
|
return nil, errors.New("title must be set if scene has no files")
|
|
}
|
|
}
|
|
|
|
if updatedScene.PrimaryFileID != nil {
|
|
newPrimaryFileID := *updatedScene.PrimaryFileID
|
|
|
|
// if file hash has changed, we should migrate generated files
|
|
// after commit
|
|
if err := originalScene.LoadFiles(ctx, r.repository.Scene); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// ensure that new primary file is associated with scene
|
|
var f *models.VideoFile
|
|
for _, ff := range originalScene.Files.List() {
|
|
if ff.ID == newPrimaryFileID {
|
|
f = ff
|
|
}
|
|
}
|
|
|
|
if f == nil {
|
|
return nil, fmt.Errorf("file with id %d not associated with scene", newPrimaryFileID)
|
|
}
|
|
}
|
|
|
|
var coverImageData []byte
|
|
if input.CoverImage != nil {
|
|
var err error
|
|
coverImageData, err = utils.ProcessImageInput(ctx, *input.CoverImage)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("processing cover image: %w", err)
|
|
}
|
|
}
|
|
|
|
scene, err := qb.UpdatePartial(ctx, sceneID, *updatedScene)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := r.sceneUpdateCoverImage(ctx, scene, coverImageData); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return scene, nil
|
|
}
|
|
|
|
func (r *mutationResolver) sceneUpdateCoverImage(ctx context.Context, s *models.Scene, coverImageData []byte) error {
|
|
if len(coverImageData) > 0 {
|
|
qb := r.repository.Scene
|
|
|
|
// update cover table
|
|
if err := qb.UpdateCover(ctx, s.ID, coverImageData); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneUpdateInput) ([]*models.Scene, error) {
|
|
sceneIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting ids: %w", err)
|
|
}
|
|
|
|
translator := changesetTranslator{
|
|
inputMap: getUpdateInputMap(ctx),
|
|
}
|
|
|
|
// Populate scene from the input
|
|
updatedScene := models.NewScenePartial()
|
|
|
|
updatedScene.Title = translator.optionalString(input.Title, "title")
|
|
updatedScene.Code = translator.optionalString(input.Code, "code")
|
|
updatedScene.Details = translator.optionalString(input.Details, "details")
|
|
updatedScene.Director = translator.optionalString(input.Director, "director")
|
|
updatedScene.Rating = translator.optionalInt(input.Rating100, "rating100")
|
|
updatedScene.Organized = translator.optionalBool(input.Organized, "organized")
|
|
|
|
updatedScene.Date, err = translator.optionalDate(input.Date, "date")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting date: %w", err)
|
|
}
|
|
updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting studio id: %w", err)
|
|
}
|
|
|
|
updatedScene.URLs = translator.optionalURLsBulk(input.Urls, input.URL)
|
|
|
|
updatedScene.PerformerIDs, err = translator.updateIdsBulk(input.PerformerIds, "performer_ids")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting performer ids: %w", err)
|
|
}
|
|
updatedScene.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting tag ids: %w", err)
|
|
}
|
|
updatedScene.GalleryIDs, err = translator.updateIdsBulk(input.GalleryIds, "gallery_ids")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
|
}
|
|
|
|
if translator.hasField("group_ids") {
|
|
updatedScene.GroupIDs, err = translator.updateGroupIDsBulk(input.GroupIds, "group_ids")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting group ids: %w", err)
|
|
}
|
|
} else if translator.hasField("movie_ids") {
|
|
updatedScene.GroupIDs, err = translator.updateGroupIDsBulk(input.MovieIds, "movie_ids")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting movie ids: %w", err)
|
|
}
|
|
}
|
|
|
|
ret := []*models.Scene{}
|
|
|
|
// Start the transaction and save the scenes
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.Scene
|
|
|
|
for _, sceneID := range sceneIDs {
|
|
scene, err := qb.UpdatePartial(ctx, sceneID, updatedScene)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ret = append(ret, scene)
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// execute post hooks outside of txn
|
|
var newRet []*models.Scene
|
|
for _, scene := range ret {
|
|
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, hook.SceneUpdatePost, input, translator.getFields())
|
|
|
|
scene, err = r.getScene(ctx, scene.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newRet = append(newRet, scene)
|
|
}
|
|
|
|
return newRet, nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) {
|
|
sceneID, err := strconv.Atoi(input.ID)
|
|
if err != nil {
|
|
return false, fmt.Errorf("converting id: %w", err)
|
|
}
|
|
|
|
fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm()
|
|
|
|
var s *models.Scene
|
|
fileDeleter := &scene.FileDeleter{
|
|
Deleter: file.NewDeleter(),
|
|
FileNamingAlgo: fileNamingAlgo,
|
|
Paths: manager.GetInstance().Paths,
|
|
}
|
|
|
|
deleteGenerated := utils.IsTrue(input.DeleteGenerated)
|
|
deleteFile := utils.IsTrue(input.DeleteFile)
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.Scene
|
|
var err error
|
|
s, err = qb.Find(ctx, sceneID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if s == nil {
|
|
return fmt.Errorf("scene with id %d not found", sceneID)
|
|
}
|
|
|
|
// kill any running encoders
|
|
manager.KillRunningStreams(s, fileNamingAlgo)
|
|
|
|
return r.sceneService.Destroy(ctx, s, fileDeleter, deleteGenerated, deleteFile)
|
|
}); err != nil {
|
|
fileDeleter.Rollback()
|
|
return false, err
|
|
}
|
|
|
|
// perform the post-commit actions
|
|
fileDeleter.Commit()
|
|
|
|
// call post hook after performing the other actions
|
|
r.hookExecutor.ExecutePostHooks(ctx, s.ID, hook.SceneDestroyPost, plugin.SceneDestroyInput{
|
|
SceneDestroyInput: input,
|
|
Checksum: s.Checksum,
|
|
OSHash: s.OSHash,
|
|
Path: s.Path,
|
|
}, nil)
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.ScenesDestroyInput) (bool, error) {
|
|
sceneIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
|
if err != nil {
|
|
return false, fmt.Errorf("converting ids: %w", err)
|
|
}
|
|
|
|
var scenes []*models.Scene
|
|
fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm()
|
|
|
|
fileDeleter := &scene.FileDeleter{
|
|
Deleter: file.NewDeleter(),
|
|
FileNamingAlgo: fileNamingAlgo,
|
|
Paths: manager.GetInstance().Paths,
|
|
}
|
|
|
|
deleteGenerated := utils.IsTrue(input.DeleteGenerated)
|
|
deleteFile := utils.IsTrue(input.DeleteFile)
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.Scene
|
|
|
|
for _, id := range sceneIDs {
|
|
scene, err := qb.Find(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if scene == nil {
|
|
return fmt.Errorf("scene with id %d not found", id)
|
|
}
|
|
|
|
scenes = append(scenes, scene)
|
|
|
|
// kill any running encoders
|
|
manager.KillRunningStreams(scene, fileNamingAlgo)
|
|
|
|
if err := r.sceneService.Destroy(ctx, scene, fileDeleter, deleteGenerated, deleteFile); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
fileDeleter.Rollback()
|
|
return false, err
|
|
}
|
|
|
|
// perform the post-commit actions
|
|
fileDeleter.Commit()
|
|
|
|
for _, scene := range scenes {
|
|
// call post hook after performing the other actions
|
|
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, hook.SceneDestroyPost, plugin.ScenesDestroyInput{
|
|
ScenesDestroyInput: input,
|
|
Checksum: scene.Checksum,
|
|
OSHash: scene.OSHash,
|
|
Path: scene.Path,
|
|
}, nil)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneAssignFile(ctx context.Context, input AssignSceneFileInput) (bool, error) {
|
|
sceneID, err := strconv.Atoi(input.SceneID)
|
|
if err != nil {
|
|
return false, fmt.Errorf("converting scene id: %w", err)
|
|
}
|
|
|
|
fileID, err := strconv.Atoi(input.FileID)
|
|
if err != nil {
|
|
return false, fmt.Errorf("converting file id: %w", err)
|
|
}
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
return r.Resolver.sceneService.AssignFile(ctx, sceneID, models.FileID(fileID))
|
|
}); err != nil {
|
|
return false, fmt.Errorf("assigning file to scene: %w", err)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneMerge(ctx context.Context, input SceneMergeInput) (*models.Scene, error) {
|
|
srcIDs, err := stringslice.StringSliceToIntSlice(input.Source)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting source ids: %w", err)
|
|
}
|
|
|
|
destID, err := strconv.Atoi(input.Destination)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting destination id: %w", err)
|
|
}
|
|
|
|
var values *models.ScenePartial
|
|
var coverImageData []byte
|
|
|
|
if input.Values != nil {
|
|
translator := changesetTranslator{
|
|
inputMap: getNamedUpdateInputMap(ctx, "input.values"),
|
|
}
|
|
|
|
values, err = scenePartialFromInput(*input.Values, translator)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if input.Values.CoverImage != nil {
|
|
var err error
|
|
coverImageData, err = utils.ProcessImageInput(ctx, *input.Values.CoverImage)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("processing cover image: %w", err)
|
|
}
|
|
}
|
|
} else {
|
|
v := models.NewScenePartial()
|
|
values = &v
|
|
}
|
|
|
|
mgr := manager.GetInstance()
|
|
fileDeleter := &scene.FileDeleter{
|
|
Deleter: file.NewDeleter(),
|
|
FileNamingAlgo: mgr.Config.GetVideoFileNamingAlgorithm(),
|
|
Paths: mgr.Paths,
|
|
}
|
|
|
|
var ret *models.Scene
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
if err := r.Resolver.sceneService.Merge(ctx, srcIDs, destID, fileDeleter, scene.MergeOptions{
|
|
ScenePartial: *values,
|
|
IncludePlayHistory: utils.IsTrue(input.PlayHistory),
|
|
IncludeOHistory: utils.IsTrue(input.OHistory),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
ret, err = r.Resolver.repository.Scene.Find(ctx, destID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ret == nil {
|
|
return fmt.Errorf("scene with id %d not found", destID)
|
|
}
|
|
|
|
return r.sceneUpdateCoverImage(ctx, ret, coverImageData)
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (r *mutationResolver) getSceneMarker(ctx context.Context, id int) (ret *models.SceneMarker, err error) {
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
ret, err = r.repository.SceneMarker.Find(ctx, id)
|
|
return err
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMarkerCreateInput) (*models.SceneMarker, error) {
|
|
sceneID, err := strconv.Atoi(input.SceneID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting scene id: %w", err)
|
|
}
|
|
|
|
primaryTagID, err := strconv.Atoi(input.PrimaryTagID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting primary tag id: %w", err)
|
|
}
|
|
|
|
// Populate a new scene marker from the input
|
|
newMarker := models.NewSceneMarker()
|
|
|
|
newMarker.Title = input.Title
|
|
newMarker.Seconds = input.Seconds
|
|
newMarker.PrimaryTagID = primaryTagID
|
|
newMarker.SceneID = sceneID
|
|
|
|
if input.EndSeconds != nil {
|
|
if err := validateSceneMarkerEndSeconds(newMarker.Seconds, *input.EndSeconds); err != nil {
|
|
return nil, err
|
|
}
|
|
newMarker.EndSeconds = input.EndSeconds
|
|
}
|
|
|
|
tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting tag ids: %w", err)
|
|
}
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.SceneMarker
|
|
|
|
err := qb.Create(ctx, &newMarker)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Save the marker tags
|
|
// If this tag is the primary tag, then let's not add it.
|
|
tagIDs = sliceutil.Exclude(tagIDs, []int{newMarker.PrimaryTagID})
|
|
return qb.UpdateTags(ctx, newMarker.ID, tagIDs)
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r.hookExecutor.ExecutePostHooks(ctx, newMarker.ID, hook.SceneMarkerCreatePost, input, nil)
|
|
return r.getSceneMarker(ctx, newMarker.ID)
|
|
}
|
|
|
|
func validateSceneMarkerEndSeconds(seconds, endSeconds float64) error {
|
|
if endSeconds < seconds {
|
|
return fmt.Errorf("end_seconds (%f) must be greater than or equal to seconds (%f)", endSeconds, seconds)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMarkerUpdateInput) (*models.SceneMarker, error) {
|
|
markerID, err := strconv.Atoi(input.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting id: %w", err)
|
|
}
|
|
|
|
translator := changesetTranslator{
|
|
inputMap: getUpdateInputMap(ctx),
|
|
}
|
|
|
|
// Populate scene marker from the input
|
|
updatedMarker := models.NewSceneMarkerPartial()
|
|
|
|
updatedMarker.Title = translator.optionalString(input.Title, "title")
|
|
updatedMarker.Seconds = translator.optionalFloat64(input.Seconds, "seconds")
|
|
updatedMarker.EndSeconds = translator.optionalFloat64(input.EndSeconds, "end_seconds")
|
|
updatedMarker.SceneID, err = translator.optionalIntFromString(input.SceneID, "scene_id")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting scene id: %w", err)
|
|
}
|
|
updatedMarker.PrimaryTagID, err = translator.optionalIntFromString(input.PrimaryTagID, "primary_tag_id")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting primary tag id: %w", err)
|
|
}
|
|
|
|
var tagIDs []int
|
|
tagIdsIncluded := translator.hasField("tag_ids")
|
|
if input.TagIds != nil {
|
|
tagIDs, err = stringslice.StringSliceToIntSlice(input.TagIds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting tag ids: %w", err)
|
|
}
|
|
}
|
|
|
|
mgr := manager.GetInstance()
|
|
|
|
fileDeleter := &scene.FileDeleter{
|
|
Deleter: file.NewDeleter(),
|
|
FileNamingAlgo: mgr.Config.GetVideoFileNamingAlgorithm(),
|
|
Paths: mgr.Paths,
|
|
}
|
|
|
|
// Start the transaction and save the scene marker
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.SceneMarker
|
|
sqb := r.repository.Scene
|
|
|
|
// check to see if timestamp was changed
|
|
existingMarker, err := qb.Find(ctx, markerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existingMarker == nil {
|
|
return fmt.Errorf("scene marker with id %d not found", markerID)
|
|
}
|
|
|
|
// Validate end_seconds
|
|
shouldValidateEndSeconds := (updatedMarker.Seconds.Set || updatedMarker.EndSeconds.Set) && !updatedMarker.EndSeconds.Null
|
|
if shouldValidateEndSeconds {
|
|
seconds := existingMarker.Seconds
|
|
if updatedMarker.Seconds.Set {
|
|
seconds = updatedMarker.Seconds.Value
|
|
}
|
|
|
|
endSeconds := existingMarker.EndSeconds
|
|
if updatedMarker.EndSeconds.Set {
|
|
endSeconds = &updatedMarker.EndSeconds.Value
|
|
}
|
|
|
|
if endSeconds != nil {
|
|
if err := validateSceneMarkerEndSeconds(seconds, *endSeconds); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
newMarker, err := qb.UpdatePartial(ctx, markerID, updatedMarker)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
existingScene, err := sqb.Find(ctx, existingMarker.SceneID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existingScene == nil {
|
|
return fmt.Errorf("scene with id %d not found", existingMarker.SceneID)
|
|
}
|
|
|
|
// remove the marker preview if the scene changed or if the timestamp was changed
|
|
if existingMarker.SceneID != newMarker.SceneID || existingMarker.Seconds != newMarker.Seconds || existingMarker.EndSeconds != newMarker.EndSeconds {
|
|
seconds := int(existingMarker.Seconds)
|
|
if err := fileDeleter.MarkMarkerFiles(existingScene, seconds); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if tagIdsIncluded {
|
|
// Save the marker tags
|
|
// If this tag is the primary tag, then let's not add it.
|
|
tagIDs = sliceutil.Exclude(tagIDs, []int{newMarker.PrimaryTagID})
|
|
if err := qb.UpdateTags(ctx, markerID, tagIDs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
fileDeleter.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
// perform the post-commit actions
|
|
fileDeleter.Commit()
|
|
|
|
r.hookExecutor.ExecutePostHooks(ctx, markerID, hook.SceneMarkerUpdatePost, input, translator.getFields())
|
|
return r.getSceneMarker(ctx, markerID)
|
|
}
|
|
|
|
func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (bool, error) {
|
|
markerID, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return false, fmt.Errorf("converting id: %w", err)
|
|
}
|
|
|
|
fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm()
|
|
|
|
fileDeleter := &scene.FileDeleter{
|
|
Deleter: file.NewDeleter(),
|
|
FileNamingAlgo: fileNamingAlgo,
|
|
Paths: manager.GetInstance().Paths,
|
|
}
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.SceneMarker
|
|
sqb := r.repository.Scene
|
|
|
|
marker, err := qb.Find(ctx, markerID)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if marker == nil {
|
|
return fmt.Errorf("scene marker with id %d not found", markerID)
|
|
}
|
|
|
|
s, err := sqb.Find(ctx, marker.SceneID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if s == nil {
|
|
return fmt.Errorf("scene with id %d not found", marker.SceneID)
|
|
}
|
|
|
|
return scene.DestroyMarker(ctx, s, marker, qb, fileDeleter)
|
|
}); err != nil {
|
|
fileDeleter.Rollback()
|
|
return false, err
|
|
}
|
|
|
|
// perform the post-commit actions
|
|
fileDeleter.Commit()
|
|
|
|
r.hookExecutor.ExecutePostHooks(ctx, markerID, hook.SceneMarkerDestroyPost, id, nil)
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneSaveActivity(ctx context.Context, id string, resumeTime *float64, playDuration *float64) (ret bool, err error) {
|
|
sceneID, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return false, fmt.Errorf("converting id: %w", err)
|
|
}
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.Scene
|
|
|
|
ret, err = qb.SaveActivity(ctx, sceneID, resumeTime, playDuration)
|
|
return err
|
|
}); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneResetActivity(ctx context.Context, id string, resetResume *bool, resetDuration *bool) (ret bool, err error) {
|
|
sceneID, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return false, fmt.Errorf("converting id: %w", err)
|
|
}
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.Scene
|
|
|
|
ret, err = qb.ResetActivity(ctx, sceneID, utils.IsTrue(resetResume), utils.IsTrue(resetDuration))
|
|
return err
|
|
}); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// deprecated
|
|
func (r *mutationResolver) SceneIncrementPlayCount(ctx context.Context, id string) (ret int, err error) {
|
|
sceneID, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("converting id: %w", err)
|
|
}
|
|
|
|
var updatedTimes []time.Time
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.Scene
|
|
|
|
updatedTimes, err = qb.AddViews(ctx, sceneID, nil)
|
|
return err
|
|
}); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return len(updatedTimes), nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneAddPlay(ctx context.Context, id string, t []*time.Time) (*HistoryMutationResult, error) {
|
|
sceneID, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting id: %w", err)
|
|
}
|
|
|
|
var times []time.Time
|
|
|
|
// convert time to local time, so that sorting is consistent
|
|
for _, tt := range t {
|
|
times = append(times, tt.Local())
|
|
}
|
|
|
|
var updatedTimes []time.Time
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.Scene
|
|
|
|
updatedTimes, err = qb.AddViews(ctx, sceneID, times)
|
|
return err
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &HistoryMutationResult{
|
|
Count: len(updatedTimes),
|
|
History: sliceutil.ValuesToPtrs(updatedTimes),
|
|
}, nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneDeletePlay(ctx context.Context, id string, t []*time.Time) (*HistoryMutationResult, error) {
|
|
sceneID, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var times []time.Time
|
|
|
|
for _, tt := range t {
|
|
times = append(times, *tt)
|
|
}
|
|
|
|
var updatedTimes []time.Time
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.Scene
|
|
|
|
updatedTimes, err = qb.DeleteViews(ctx, sceneID, times)
|
|
return err
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &HistoryMutationResult{
|
|
Count: len(updatedTimes),
|
|
History: sliceutil.ValuesToPtrs(updatedTimes),
|
|
}, nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneResetPlayCount(ctx context.Context, id string) (ret int, err error) {
|
|
sceneID, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.Scene
|
|
|
|
ret, err = qb.DeleteAllViews(ctx, sceneID)
|
|
return err
|
|
}); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// deprecated
|
|
func (r *mutationResolver) SceneIncrementO(ctx context.Context, id string) (ret int, err error) {
|
|
sceneID, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("converting id: %w", err)
|
|
}
|
|
|
|
var updatedTimes []time.Time
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.Scene
|
|
|
|
updatedTimes, err = qb.AddO(ctx, sceneID, nil)
|
|
return err
|
|
}); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return len(updatedTimes), nil
|
|
}
|
|
|
|
// deprecated
|
|
func (r *mutationResolver) SceneDecrementO(ctx context.Context, id string) (ret int, err error) {
|
|
sceneID, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("converting id: %w", err)
|
|
}
|
|
|
|
var updatedTimes []time.Time
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.Scene
|
|
|
|
updatedTimes, err = qb.DeleteO(ctx, sceneID, nil)
|
|
return err
|
|
}); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return len(updatedTimes), nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (ret int, err error) {
|
|
sceneID, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("converting id: %w", err)
|
|
}
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.Scene
|
|
|
|
ret, err = qb.ResetO(ctx, sceneID)
|
|
return err
|
|
}); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneAddO(ctx context.Context, id string, t []*time.Time) (*HistoryMutationResult, error) {
|
|
sceneID, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting id: %w", err)
|
|
}
|
|
|
|
var times []time.Time
|
|
|
|
// convert time to local time, so that sorting is consistent
|
|
for _, tt := range t {
|
|
times = append(times, tt.Local())
|
|
}
|
|
|
|
var updatedTimes []time.Time
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.Scene
|
|
|
|
updatedTimes, err = qb.AddO(ctx, sceneID, times)
|
|
return err
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &HistoryMutationResult{
|
|
Count: len(updatedTimes),
|
|
History: sliceutil.ValuesToPtrs(updatedTimes),
|
|
}, nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneDeleteO(ctx context.Context, id string, t []*time.Time) (*HistoryMutationResult, error) {
|
|
sceneID, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting id: %w", err)
|
|
}
|
|
|
|
var times []time.Time
|
|
|
|
for _, tt := range t {
|
|
times = append(times, *tt)
|
|
}
|
|
|
|
var updatedTimes []time.Time
|
|
|
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
|
qb := r.repository.Scene
|
|
|
|
updatedTimes, err = qb.DeleteO(ctx, sceneID, times)
|
|
return err
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &HistoryMutationResult{
|
|
Count: len(updatedTimes),
|
|
History: sliceutil.ValuesToPtrs(updatedTimes),
|
|
}, nil
|
|
}
|
|
|
|
func (r *mutationResolver) SceneGenerateScreenshot(ctx context.Context, id string, at *float64) (string, error) {
|
|
if at != nil {
|
|
manager.GetInstance().GenerateScreenshot(ctx, id, *at)
|
|
} else {
|
|
manager.GetInstance().GenerateDefaultScreenshot(ctx, id)
|
|
}
|
|
|
|
return "todo", nil
|
|
}
|