From 74cef93d1939a84dd3e4a2459a2fbcd15bdb6133 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 31 May 2023 11:06:01 +1000 Subject: [PATCH] Update gallery UpdatedAt timestamp on contents change (#3771) * Update gallery updatedAt on content change * Update gallery in UI on image change --- internal/api/resolver_mutation_image.go | 23 +++++++ internal/manager/repository.go | 2 + pkg/gallery/service.go | 5 ++ pkg/gallery/update.go | 27 ++++++-- pkg/models/update.go | 42 +++++++++++ pkg/models/update_test.go | 92 +++++++++++++++++++++++++ ui/v2.5/src/core/StashService.ts | 2 + 7 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 pkg/models/update_test.go diff --git a/internal/api/resolver_mutation_image.go b/internal/api/resolver_mutation_image.go index 353dab744..24b81967a 100644 --- a/internal/api/resolver_mutation_image.go +++ b/internal/api/resolver_mutation_image.go @@ -10,6 +10,7 @@ import ( "github.com/stashapp/stash/pkg/image" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/plugin" + "github.com/stashapp/stash/pkg/sliceutil/intslice" "github.com/stashapp/stash/pkg/sliceutil/stringslice" "github.com/stashapp/stash/pkg/utils" ) @@ -138,6 +139,8 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp } } + var updatedGalleryIDs []int + if translator.hasField("gallery_ids") { updatedImage.GalleryIDs, err = translateUpdateIDs(input.GalleryIds, models.RelationshipUpdateModeSet) if err != nil { @@ -152,6 +155,8 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp if err := r.galleryService.ValidateImageGalleryChange(ctx, i, *updatedImage.GalleryIDs); err != nil { return nil, err } + + updatedGalleryIDs = updatedImage.GalleryIDs.ImpactedIDs(i.GalleryIDs.List()) } if translator.hasField("performer_ids") { @@ -174,6 +179,13 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp return nil, err } + // #3759 - update all impacted galleries + for _, galleryID := range updatedGalleryIDs { + if err := r.galleryService.Updated(ctx, galleryID); err != nil { + return nil, fmt.Errorf("updating gallery %d: %w", galleryID, err) + } + } + return image, nil } @@ -223,6 +235,7 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU // Start the transaction and save the image marker if err := r.withTxn(ctx, func(ctx context.Context) error { + var updatedGalleryIDs []int qb := r.repository.Image for _, imageID := range imageIDs { @@ -244,6 +257,9 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU if err := r.galleryService.ValidateImageGalleryChange(ctx, i, *updatedImage.GalleryIDs); err != nil { return err } + + thisUpdatedGalleryIDs := updatedImage.GalleryIDs.ImpactedIDs(i.GalleryIDs.List()) + updatedGalleryIDs = intslice.IntAppendUniques(updatedGalleryIDs, thisUpdatedGalleryIDs) } image, err := qb.UpdatePartial(ctx, imageID, updatedImage) @@ -254,6 +270,13 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU ret = append(ret, image) } + // #3759 - update all impacted galleries + for _, galleryID := range updatedGalleryIDs { + if err := r.galleryService.Updated(ctx, galleryID); err != nil { + return fmt.Errorf("updating gallery %d: %w", galleryID, err) + } + } + return nil }); err != nil { return nil, err diff --git a/internal/manager/repository.go b/internal/manager/repository.go index dd49c4af7..55fea1672 100644 --- a/internal/manager/repository.go +++ b/internal/manager/repository.go @@ -113,4 +113,6 @@ type GalleryService interface { Destroy(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) ([]*models.Image, error) ValidateImageGalleryChange(ctx context.Context, i *models.Image, updateIDs models.UpdateIDs) error + + Updated(ctx context.Context, galleryID int) error } diff --git a/pkg/gallery/service.go b/pkg/gallery/service.go index acf70763f..7dfc3857f 100644 --- a/pkg/gallery/service.go +++ b/pkg/gallery/service.go @@ -18,6 +18,11 @@ type Repository interface { Destroy(ctx context.Context, id int) error models.FileLoader ImageUpdater + PartialUpdater +} + +type PartialUpdater interface { + UpdatePartial(ctx context.Context, id int, updatedGallery models.GalleryPartial) (*models.Gallery, error) } type ImageFinder interface { diff --git a/pkg/gallery/update.go b/pkg/gallery/update.go index 5350499ac..72f479bea 100644 --- a/pkg/gallery/update.go +++ b/pkg/gallery/update.go @@ -2,20 +2,25 @@ package gallery import ( "context" + "fmt" + "time" "github.com/stashapp/stash/pkg/models" ) -type PartialUpdater interface { - UpdatePartial(ctx context.Context, id int, updatedGallery models.GalleryPartial) (*models.Gallery, error) -} - type ImageUpdater interface { GetImageIDs(ctx context.Context, galleryID int) ([]int, error) AddImages(ctx context.Context, galleryID int, imageIDs ...int) error RemoveImages(ctx context.Context, galleryID int, imageIDs ...int) error } +func (s *Service) Updated(ctx context.Context, galleryID int) error { + _, err := s.Repository.UpdatePartial(ctx, galleryID, models.GalleryPartial{ + UpdatedAt: models.NewOptionalTime(time.Now()), + }) + return err +} + // AddImages adds images to the provided gallery. // It returns an error if the gallery does not support adding images, or if // the operation fails. @@ -24,7 +29,12 @@ func (s *Service) AddImages(ctx context.Context, g *models.Gallery, toAdd ...int return err } - return s.Repository.AddImages(ctx, g.ID, toAdd...) + if err := s.Repository.AddImages(ctx, g.ID, toAdd...); err != nil { + return fmt.Errorf("failed to add images to gallery: %w", err) + } + + // #3759 - update the gallery's UpdatedAt timestamp + return s.Updated(ctx, g.ID) } // RemoveImages removes images from the provided gallery. @@ -36,7 +46,12 @@ func (s *Service) RemoveImages(ctx context.Context, g *models.Gallery, toRemove return err } - return s.Repository.RemoveImages(ctx, g.ID, toRemove...) + if err := s.Repository.RemoveImages(ctx, g.ID, toRemove...); err != nil { + return fmt.Errorf("failed to remove images from gallery: %w", err) + } + + // #3759 - update the gallery's UpdatedAt timestamp + return s.Updated(ctx, g.ID) } func AddPerformer(ctx context.Context, qb PartialUpdater, o *models.Gallery, performerID int) error { diff --git a/pkg/models/update.go b/pkg/models/update.go index fbfab3d30..ffa793bda 100644 --- a/pkg/models/update.go +++ b/pkg/models/update.go @@ -64,6 +64,48 @@ func (u *UpdateIDs) IDStrings() []string { return intslice.IntSliceToStringSlice(u.IDs) } +// GetImpactedIDs returns the IDs that will be impacted by the update. +// If the update is to add IDs, then the impacted IDs are the IDs being added. +// If the update is to remove IDs, then the impacted IDs are the IDs being removed. +// If the update is to set IDs, then the impacted IDs are the IDs being removed and the IDs being added. +// Any IDs that are already present and are being added are not returned. +// Likewise, any IDs that are not present that are being removed are not returned. +func (u *UpdateIDs) ImpactedIDs(existing []int) []int { + if u == nil { + return nil + } + + switch u.Mode { + case RelationshipUpdateModeAdd: + return intslice.IntExclude(u.IDs, existing) + case RelationshipUpdateModeRemove: + return intslice.IntIntercect(existing, u.IDs) + case RelationshipUpdateModeSet: + // get the difference between the two lists + return intslice.IntNotIntersect(existing, u.IDs) + } + + return nil +} + +// GetEffectiveIDs returns the new IDs that will be effective after the update. +func (u *UpdateIDs) EffectiveIDs(existing []int) []int { + if u == nil { + return nil + } + + switch u.Mode { + case RelationshipUpdateModeAdd: + return intslice.IntAppendUniques(existing, u.IDs) + case RelationshipUpdateModeRemove: + return intslice.IntExclude(existing, u.IDs) + case RelationshipUpdateModeSet: + return u.IDs + } + + return nil +} + type UpdateStrings struct { Values []string `json:"values"` Mode RelationshipUpdateMode `json:"mode"` diff --git a/pkg/models/update_test.go b/pkg/models/update_test.go new file mode 100644 index 000000000..0baf7926f --- /dev/null +++ b/pkg/models/update_test.go @@ -0,0 +1,92 @@ +package models + +import ( + "reflect" + "testing" +) + +func TestUpdateIDs_ImpactedIDs(t *testing.T) { + tests := []struct { + name string + IDs []int + Mode RelationshipUpdateMode + existing []int + want []int + }{ + { + name: "add", + IDs: []int{1, 2, 3}, + Mode: RelationshipUpdateModeAdd, + existing: []int{1, 2}, + want: []int{3}, + }, + { + name: "remove", + IDs: []int{1, 2, 3}, + Mode: RelationshipUpdateModeRemove, + existing: []int{1, 2}, + want: []int{1, 2}, + }, + { + name: "set", + IDs: []int{1, 2, 3}, + Mode: RelationshipUpdateModeSet, + existing: []int{1, 2}, + want: []int{3}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := &UpdateIDs{ + IDs: tt.IDs, + Mode: tt.Mode, + } + if got := u.ImpactedIDs(tt.existing); !reflect.DeepEqual(got, tt.want) { + t.Errorf("UpdateIDs.ImpactedIDs() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUpdateIDs_EffectiveIDs(t *testing.T) { + tests := []struct { + name string + IDs []int + Mode RelationshipUpdateMode + existing []int + want []int + }{ + { + name: "add", + IDs: []int{2, 3}, + Mode: RelationshipUpdateModeAdd, + existing: []int{1, 2}, + want: []int{1, 2, 3}, + }, + { + name: "remove", + IDs: []int{2, 3}, + Mode: RelationshipUpdateModeRemove, + existing: []int{1, 2}, + want: []int{1}, + }, + { + name: "set", + IDs: []int{1, 2, 3}, + Mode: RelationshipUpdateModeSet, + existing: []int{1, 2}, + want: []int{1, 2, 3}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := &UpdateIDs{ + IDs: tt.IDs, + Mode: tt.Mode, + } + if got := u.EffectiveIDs(tt.existing); !reflect.DeepEqual(got, tt.want) { + t.Errorf("UpdateIDs.EffectiveIDs() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index 10bb49d3a..7e79db3b1 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -734,6 +734,7 @@ export const mutateAddGalleryImages = (input: GQL.GalleryAddInput) => mutation: GQL.AddGalleryImagesDocument, variables: input, update: deleteCache(galleryMutationImpactedQueries), + refetchQueries: getQueryNames([GQL.FindGalleryDocument]), }); export const mutateRemoveGalleryImages = (input: GQL.GalleryRemoveInput) => @@ -741,6 +742,7 @@ export const mutateRemoveGalleryImages = (input: GQL.GalleryRemoveInput) => mutation: GQL.RemoveGalleryImagesDocument, variables: input, update: deleteCache(galleryMutationImpactedQueries), + refetchQueries: getQueryNames([GQL.FindGalleryDocument]), }); export const mutateGallerySetPrimaryFile = (id: string, fileID: string) =>