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(input.StashIds) 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) } newScene.Movies, err = translator.relatedMovies(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) } updatedScene.MovieIDs, err = translator.updateMovieIDs(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) } updatedScene.MovieIDs, err = translator.updateMovieIDsBulk(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 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 (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.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) } 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 { 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 } // 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 }