stash/internal/api/resolver_mutation_scene.go
MinasukiHikimuna 0d40056f8c
Markers can have end time (#5311)
* 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>
2024-11-02 11:55:48 +11:00

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
}