diff --git a/gqlgen.yml b/gqlgen.yml index bfa5654c1b0..dc101f03c3e 100644 --- a/gqlgen.yml +++ b/gqlgen.yml @@ -129,9 +129,6 @@ models: model: github.com/stashapp/stash/internal/identify.FieldStrategy ScraperSource: model: github.com/stashapp/stash/pkg/scraper.Source - # rebind inputs to types - StashIDInput: - model: github.com/stashapp/stash/pkg/models.StashID IdentifySourceInput: model: github.com/stashapp/stash/internal/identify.Source IdentifyFieldOptionsInput: diff --git a/graphql/schema/types/stash-box.graphql b/graphql/schema/types/stash-box.graphql index 71ea757f443..d1da8c74a76 100644 --- a/graphql/schema/types/stash-box.graphql +++ b/graphql/schema/types/stash-box.graphql @@ -13,11 +13,13 @@ input StashBoxInput { type StashID { endpoint: String! stash_id: String! + updated_at: Time! } input StashIDInput { endpoint: String! stash_id: String! + updated_at: Time } input StashBoxFingerprintSubmissionInput { diff --git a/internal/api/changeset_translator.go b/internal/api/changeset_translator.go index 1170088aac9..5c81c12cb09 100644 --- a/internal/api/changeset_translator.go +++ b/internal/api/changeset_translator.go @@ -335,13 +335,13 @@ func (t changesetTranslator) updateStringsBulk(value *BulkUpdateStrings, field s } } -func (t changesetTranslator) updateStashIDs(value []models.StashID, field string) *models.UpdateStashIDs { +func (t changesetTranslator) updateStashIDs(value models.StashIDInputs, field string) *models.UpdateStashIDs { if !t.hasField(field) { return nil } return &models.UpdateStashIDs{ - StashIDs: value, + StashIDs: value.ToStashIDs(), Mode: models.RelationshipUpdateModeSet, } } diff --git a/internal/api/resolver_mutation_performer.go b/internal/api/resolver_mutation_performer.go index 7263cc70966..87f0883ed24 100644 --- a/internal/api/resolver_mutation_performer.go +++ b/internal/api/resolver_mutation_performer.go @@ -58,7 +58,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per newPerformer.Height = input.HeightCm newPerformer.Weight = input.Weight newPerformer.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag) - newPerformer.StashIDs = models.NewRelatedStashIDs(input.StashIds) + newPerformer.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs()) newPerformer.URLs = models.NewRelatedStrings([]string{}) if input.URL != nil { diff --git a/internal/api/resolver_mutation_scene.go b/internal/api/resolver_mutation_scene.go index ca99dafc150..b0c6ac8b5aa 100644 --- a/internal/api/resolver_mutation_scene.go +++ b/internal/api/resolver_mutation_scene.go @@ -50,7 +50,7 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCr newScene.Director = translator.string(input.Director) newScene.Rating = input.Rating100 newScene.Organized = translator.bool(input.Organized) - newScene.StashIDs = models.NewRelatedStashIDs(input.StashIds) + newScene.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs()) newScene.Date, err = translator.datePtr(input.Date) if err != nil { diff --git a/internal/api/resolver_mutation_studio.go b/internal/api/resolver_mutation_studio.go index a33e5d9b676..727951755e9 100644 --- a/internal/api/resolver_mutation_studio.go +++ b/internal/api/resolver_mutation_studio.go @@ -39,7 +39,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio newStudio.Details = translator.string(input.Details) newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag) newStudio.Aliases = models.NewRelatedStrings(input.Aliases) - newStudio.StashIDs = models.NewRelatedStashIDs(input.StashIds) + newStudio.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs()) var err error diff --git a/internal/identify/identify.go b/internal/identify/identify.go index dca1a68d71b..70d9322274a 100644 --- a/internal/identify/identify.go +++ b/internal/identify/identify.go @@ -245,7 +245,18 @@ func (t *SceneIdentifier) getSceneUpdater(ctx context.Context, s *models.Scene, } } - stashIDs, err := rel.stashIDs(ctx) + // SetCoverImage defaults to true if unset + if options.SetCoverImage == nil || *options.SetCoverImage { + ret.CoverImage, err = rel.cover(ctx) + if err != nil { + return nil, err + } + } + + // if anything changed, also update the updated at time on the applicable stash id + changed := !ret.IsEmpty() + + stashIDs, err := rel.stashIDs(ctx, changed) if err != nil { return nil, err } @@ -256,14 +267,6 @@ func (t *SceneIdentifier) getSceneUpdater(ctx context.Context, s *models.Scene, } } - // SetCoverImage defaults to true if unset - if options.SetCoverImage == nil || *options.SetCoverImage { - ret.CoverImage, err = rel.cover(ctx) - if err != nil { - return nil, err - } - } - return ret, nil } diff --git a/internal/identify/scene.go b/internal/identify/scene.go index 05f1ba90076..847a140c5ae 100644 --- a/internal/identify/scene.go +++ b/internal/identify/scene.go @@ -7,6 +7,7 @@ import ( "fmt" "strconv" "strings" + "time" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" @@ -182,7 +183,13 @@ func (g sceneRelationships) tags(ctx context.Context) ([]int, error) { return tagIDs, nil } -func (g sceneRelationships) stashIDs(ctx context.Context) ([]models.StashID, error) { +// stashIDs returns the updated stash IDs for the scene +// returns nil if not applicable or no changes were made +// if setUpdateTime is true, then the updated_at field will be set to the current time +// for the applicable matching stash ID +func (g sceneRelationships) stashIDs(ctx context.Context, setUpdateTime bool) ([]models.StashID, error) { + updateTime := time.Now() + remoteSiteID := g.result.result.RemoteSiteID fieldStrategy := g.fieldOptions["stash_ids"] target := g.scene @@ -199,7 +206,7 @@ func (g sceneRelationships) stashIDs(ctx context.Context) ([]models.StashID, err strategy = fieldStrategy.Strategy } - var stashIDs []models.StashID + var stashIDs models.StashIDs originalStashIDs := target.StashIDs.List() if strategy == FieldStrategyMerge { @@ -208,15 +215,17 @@ func (g sceneRelationships) stashIDs(ctx context.Context) ([]models.StashID, err stashIDs = append(stashIDs, originalStashIDs...) } + // find and update the stash id if it exists for i, stashID := range stashIDs { if endpoint == stashID.Endpoint { // if stashID is the same, then don't set - if stashID.StashID == *remoteSiteID { + if !setUpdateTime && stashID.StashID == *remoteSiteID { return nil, nil } // replace the stash id and return stashID.StashID = *remoteSiteID + stashID.UpdatedAt = updateTime stashIDs[i] = stashID return stashIDs, nil } @@ -224,11 +233,14 @@ func (g sceneRelationships) stashIDs(ctx context.Context) ([]models.StashID, err // not found, create new entry stashIDs = append(stashIDs, models.StashID{ - StashID: *remoteSiteID, - Endpoint: endpoint, + StashID: *remoteSiteID, + Endpoint: endpoint, + UpdatedAt: updateTime, }) - if sliceutil.SliceSame(originalStashIDs, stashIDs) { + // don't return if nothing was changed + // if we're setting update time, then we always return + if !setUpdateTime && stashIDs.HasSameStashIDs(originalStashIDs) { return nil, nil } diff --git a/internal/identify/scene_test.go b/internal/identify/scene_test.go index 272ca43cb1d..7587eee7e27 100644 --- a/internal/identify/scene_test.go +++ b/internal/identify/scene_test.go @@ -5,6 +5,7 @@ import ( "reflect" "strconv" "testing" + "time" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/mocks" @@ -548,8 +549,9 @@ func Test_sceneRelationships_stashIDs(t *testing.T) { ID: sceneWithStashID, StashIDs: models.NewRelatedStashIDs([]models.StashID{ { - StashID: remoteSiteID, - Endpoint: existingEndpoint, + StashID: remoteSiteID, + Endpoint: existingEndpoint, + UpdatedAt: time.Time{}, }, }), } @@ -561,14 +563,17 @@ func Test_sceneRelationships_stashIDs(t *testing.T) { fieldOptions: make(map[string]*FieldOptions), } + setTime := time.Now() + tests := []struct { - name string - scene *models.Scene - fieldOptions *FieldOptions - endpoint string - remoteSiteID *string - want []models.StashID - wantErr bool + name string + scene *models.Scene + fieldOptions *FieldOptions + endpoint string + remoteSiteID *string + setUpdateTime bool + want []models.StashID + wantErr bool }{ { "ignore", @@ -578,6 +583,7 @@ func Test_sceneRelationships_stashIDs(t *testing.T) { }, newEndpoint, &remoteSiteID, + false, nil, false, }, @@ -587,6 +593,7 @@ func Test_sceneRelationships_stashIDs(t *testing.T) { defaultOptions, "", &remoteSiteID, + false, nil, false, }, @@ -596,6 +603,7 @@ func Test_sceneRelationships_stashIDs(t *testing.T) { defaultOptions, newEndpoint, nil, + false, nil, false, }, @@ -605,19 +613,38 @@ func Test_sceneRelationships_stashIDs(t *testing.T) { defaultOptions, existingEndpoint, &remoteSiteID, + false, nil, false, }, + { + "merge existing set update time", + sceneWithStashIDs, + defaultOptions, + existingEndpoint, + &remoteSiteID, + true, + []models.StashID{ + { + StashID: remoteSiteID, + Endpoint: existingEndpoint, + UpdatedAt: setTime, + }, + }, + false, + }, { "merge existing new value", sceneWithStashIDs, defaultOptions, existingEndpoint, &newRemoteSiteID, + false, []models.StashID{ { - StashID: newRemoteSiteID, - Endpoint: existingEndpoint, + StashID: newRemoteSiteID, + Endpoint: existingEndpoint, + UpdatedAt: setTime, }, }, false, @@ -628,14 +655,17 @@ func Test_sceneRelationships_stashIDs(t *testing.T) { defaultOptions, newEndpoint, &newRemoteSiteID, + false, []models.StashID{ { - StashID: remoteSiteID, - Endpoint: existingEndpoint, + StashID: remoteSiteID, + Endpoint: existingEndpoint, + UpdatedAt: time.Time{}, }, { - StashID: newRemoteSiteID, - Endpoint: newEndpoint, + StashID: newRemoteSiteID, + Endpoint: newEndpoint, + UpdatedAt: setTime, }, }, false, @@ -648,10 +678,12 @@ func Test_sceneRelationships_stashIDs(t *testing.T) { }, newEndpoint, &newRemoteSiteID, + false, []models.StashID{ { - StashID: newRemoteSiteID, - Endpoint: newEndpoint, + StashID: newRemoteSiteID, + Endpoint: newEndpoint, + UpdatedAt: setTime, }, }, false, @@ -664,9 +696,28 @@ func Test_sceneRelationships_stashIDs(t *testing.T) { }, existingEndpoint, &remoteSiteID, + false, nil, false, }, + { + "overwrite same set update time", + sceneWithStashIDs, + &FieldOptions{ + Strategy: FieldStrategyOverwrite, + }, + existingEndpoint, + &remoteSiteID, + true, + []models.StashID{ + { + StashID: remoteSiteID, + Endpoint: existingEndpoint, + UpdatedAt: setTime, + }, + }, + false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -681,11 +732,20 @@ func Test_sceneRelationships_stashIDs(t *testing.T) { }, } - got, err := tr.stashIDs(testCtx) + got, err := tr.stashIDs(testCtx, tt.setUpdateTime) + if (err != nil) != tt.wantErr { t.Errorf("sceneRelationships.stashIDs() error = %v, wantErr %v", err, tt.wantErr) return } + + // massage updatedAt times to be consistent for comparison + for i := range got { + if !got[i].UpdatedAt.IsZero() { + got[i].UpdatedAt = setTime + } + } + if !reflect.DeepEqual(got, tt.want) { t.Errorf("sceneRelationships.stashIDs() = %+v, want %+v", got, tt.want) } diff --git a/pkg/models/model_scene.go b/pkg/models/model_scene.go index 3f26a8cb6d8..cf04993882c 100644 --- a/pkg/models/model_scene.go +++ b/pkg/models/model_scene.go @@ -192,9 +192,9 @@ func (s ScenePartial) UpdateInput(id int) SceneUpdateInput { dateStr = &v } - var stashIDs []StashID + var stashIDs StashIDs if s.StashIDs != nil { - stashIDs = s.StashIDs.StashIDs + stashIDs = StashIDs(s.StashIDs.StashIDs) } ret := SceneUpdateInput{ @@ -212,7 +212,7 @@ func (s ScenePartial) UpdateInput(id int) SceneUpdateInput { PerformerIds: s.PerformerIDs.IDStrings(), Movies: s.GroupIDs.SceneMovieInputs(), TagIds: s.TagIDs.IDStrings(), - StashIds: stashIDs, + StashIds: stashIDs.ToStashIDInputs(), } return ret diff --git a/pkg/models/model_scraped_item.go b/pkg/models/model_scraped_item.go index 35f781109cb..43e3e985b3b 100644 --- a/pkg/models/model_scraped_item.go +++ b/pkg/models/model_scraped_item.go @@ -3,6 +3,7 @@ package models import ( "context" "strconv" + "time" "github.com/stashapp/stash/pkg/sliceutil/stringslice" "github.com/stashapp/stash/pkg/utils" @@ -29,8 +30,9 @@ func (s *ScrapedStudio) ToStudio(endpoint string, excluded map[string]bool) *Stu if s.RemoteSiteID != nil && endpoint != "" { ret.StashIDs = NewRelatedStashIDs([]StashID{ { - Endpoint: endpoint, - StashID: *s.RemoteSiteID, + Endpoint: endpoint, + StashID: *s.RemoteSiteID, + UpdatedAt: time.Now(), }, }) } @@ -65,6 +67,7 @@ func (s *ScrapedStudio) GetImage(ctx context.Context, excluded map[string]bool) func (s *ScrapedStudio) ToPartial(id string, endpoint string, excluded map[string]bool, existingStashIDs []StashID) StudioPartial { ret := NewStudioPartial() ret.ID, _ = strconv.Atoi(id) + currentTime := time.Now() if s.Name != "" && !excluded["name"] { ret.Name = NewOptionalString(s.Name) @@ -90,8 +93,9 @@ func (s *ScrapedStudio) ToPartial(id string, endpoint string, excluded map[strin Mode: RelationshipUpdateModeSet, } ret.StashIDs.Set(StashID{ - Endpoint: endpoint, - StashID: *s.RemoteSiteID, + Endpoint: endpoint, + StashID: *s.RemoteSiteID, + UpdatedAt: currentTime, }) } @@ -137,6 +141,7 @@ func (ScrapedPerformer) IsScrapedContent() {} func (p *ScrapedPerformer) ToPerformer(endpoint string, excluded map[string]bool) *Performer { ret := NewPerformer() + currentTime := time.Now() ret.Name = *p.Name if p.Aliases != nil && !excluded["aliases"] { @@ -244,8 +249,9 @@ func (p *ScrapedPerformer) ToPerformer(endpoint string, excluded map[string]bool if p.RemoteSiteID != nil && endpoint != "" { ret.StashIDs = NewRelatedStashIDs([]StashID{ { - Endpoint: endpoint, - StashID: *p.RemoteSiteID, + Endpoint: endpoint, + StashID: *p.RemoteSiteID, + UpdatedAt: currentTime, }, }) } @@ -375,8 +381,9 @@ func (p *ScrapedPerformer) ToPartial(endpoint string, excluded map[string]bool, Mode: RelationshipUpdateModeSet, } ret.StashIDs.Set(StashID{ - Endpoint: endpoint, - StashID: *p.RemoteSiteID, + Endpoint: endpoint, + StashID: *p.RemoteSiteID, + UpdatedAt: time.Now(), }) } diff --git a/pkg/models/model_scraped_item_test.go b/pkg/models/model_scraped_item_test.go index 87ce2ad57dc..1e8edccb410 100644 --- a/pkg/models/model_scraped_item_test.go +++ b/pkg/models/model_scraped_item_test.go @@ -87,6 +87,11 @@ func Test_scrapedToStudioInput(t *testing.T) { got.CreatedAt = time.Time{} got.UpdatedAt = time.Time{} + if got.StashIDs.Loaded() && len(got.StashIDs.List()) > 0 { + for stid := range got.StashIDs.List() { + got.StashIDs.List()[stid].UpdatedAt = time.Time{} + } + } assert.Equal(t, tt.want, got) }) } @@ -243,6 +248,12 @@ func Test_scrapedToPerformerInput(t *testing.T) { got.CreatedAt = time.Time{} got.UpdatedAt = time.Time{} + + if got.StashIDs.Loaded() && len(got.StashIDs.List()) > 0 { + for stid := range got.StashIDs.List() { + got.StashIDs.List()[stid].UpdatedAt = time.Time{} + } + } assert.Equal(t, tt.want, got) }) } @@ -263,7 +274,7 @@ func TestScrapedStudio_ToPartial(t *testing.T) { images = []string{image} existingEndpoint = "existingEndpoint" - existingStashID = StashID{"existingStashID", existingEndpoint} + existingStashID = StashID{"existingStashID", existingEndpoint, time.Time{}} existingStashIDs = []StashID{existingStashID} ) @@ -362,6 +373,11 @@ func TestScrapedStudio_ToPartial(t *testing.T) { // unset updatedAt - we don't need to compare it got.UpdatedAt = OptionalTime{} + if got.StashIDs != nil && len(got.StashIDs.StashIDs) > 0 { + for stid := range got.StashIDs.StashIDs { + got.StashIDs.StashIDs[stid].UpdatedAt = time.Time{} + } + } assert.Equal(t, tt.want, got) }) diff --git a/pkg/models/performer.go b/pkg/models/performer.go index b14f60044be..47394996d3f 100644 --- a/pkg/models/performer.go +++ b/pkg/models/performer.go @@ -226,14 +226,14 @@ type PerformerCreateInput struct { Favorite *bool `json:"favorite"` TagIds []string `json:"tag_ids"` // This should be a URL or a base64 encoded data URL - Image *string `json:"image"` - StashIds []StashID `json:"stash_ids"` - Rating100 *int `json:"rating100"` - Details *string `json:"details"` - DeathDate *string `json:"death_date"` - HairColor *string `json:"hair_color"` - Weight *int `json:"weight"` - IgnoreAutoTag *bool `json:"ignore_auto_tag"` + Image *string `json:"image"` + StashIds []StashIDInput `json:"stash_ids"` + Rating100 *int `json:"rating100"` + Details *string `json:"details"` + DeathDate *string `json:"death_date"` + HairColor *string `json:"hair_color"` + Weight *int `json:"weight"` + IgnoreAutoTag *bool `json:"ignore_auto_tag"` } type PerformerUpdateInput struct { @@ -263,12 +263,12 @@ type PerformerUpdateInput struct { Favorite *bool `json:"favorite"` TagIds []string `json:"tag_ids"` // This should be a URL or a base64 encoded data URL - Image *string `json:"image"` - StashIds []StashID `json:"stash_ids"` - Rating100 *int `json:"rating100"` - Details *string `json:"details"` - DeathDate *string `json:"death_date"` - HairColor *string `json:"hair_color"` - Weight *int `json:"weight"` - IgnoreAutoTag *bool `json:"ignore_auto_tag"` + Image *string `json:"image"` + StashIds []StashIDInput `json:"stash_ids"` + Rating100 *int `json:"rating100"` + Details *string `json:"details"` + DeathDate *string `json:"death_date"` + HairColor *string `json:"hair_color"` + Weight *int `json:"weight"` + IgnoreAutoTag *bool `json:"ignore_auto_tag"` } diff --git a/pkg/models/scene.go b/pkg/models/scene.go index 48317240276..c7be343d98c 100644 --- a/pkg/models/scene.go +++ b/pkg/models/scene.go @@ -163,8 +163,8 @@ type SceneCreateInput struct { Groups []SceneGroupInput `json:"groups"` TagIds []string `json:"tag_ids"` // This should be a URL or a base64 encoded data URL - CoverImage *string `json:"cover_image"` - StashIds []StashID `json:"stash_ids"` + CoverImage *string `json:"cover_image"` + StashIds []StashIDInput `json:"stash_ids"` // The first id will be assigned as primary. // Files will be reassigned from existing scenes if applicable. // Files must not already be primary for another scene. @@ -191,12 +191,12 @@ type SceneUpdateInput struct { Groups []SceneGroupInput `json:"groups"` TagIds []string `json:"tag_ids"` // This should be a URL or a base64 encoded data URL - CoverImage *string `json:"cover_image"` - StashIds []StashID `json:"stash_ids"` - ResumeTime *float64 `json:"resume_time"` - PlayDuration *float64 `json:"play_duration"` - PlayCount *int `json:"play_count"` - PrimaryFileID *string `json:"primary_file_id"` + CoverImage *string `json:"cover_image"` + StashIds []StashIDInput `json:"stash_ids"` + ResumeTime *float64 `json:"resume_time"` + PlayDuration *float64 `json:"play_duration"` + PlayCount *int `json:"play_count"` + PrimaryFileID *string `json:"primary_file_id"` } type SceneDestroyInput struct { diff --git a/pkg/models/stash_ids.go b/pkg/models/stash_ids.go index fcc2bdec0c2..7751c2ef01c 100644 --- a/pkg/models/stash_ids.go +++ b/pkg/models/stash_ids.go @@ -1,8 +1,89 @@ package models +import ( + "slices" + "time" +) + type StashID struct { - StashID string `db:"stash_id" json:"stash_id"` - Endpoint string `db:"endpoint" json:"endpoint"` + StashID string `db:"stash_id" json:"stash_id"` + Endpoint string `db:"endpoint" json:"endpoint"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +func (s StashID) ToStashIDInput() StashIDInput { + t := s.UpdatedAt + return StashIDInput{ + StashID: s.StashID, + Endpoint: s.Endpoint, + UpdatedAt: &t, + } +} + +type StashIDs []StashID + +func (s StashIDs) ToStashIDInputs() StashIDInputs { + if s == nil { + return nil + } + + ret := make(StashIDInputs, len(s)) + for i, v := range s { + ret[i] = v.ToStashIDInput() + } + return ret +} + +// HasSameStashIDs returns true if the two lists of StashIDs are the same, ignoring order and updated at time. +func (s StashIDs) HasSameStashIDs(other StashIDs) bool { + if len(s) != len(other) { + return false + } + + for _, v := range s { + if !slices.ContainsFunc(other, func(o StashID) bool { + return o.StashID == v.StashID && o.Endpoint == v.Endpoint + }) { + return false + } + } + + return true +} + +type StashIDInput struct { + StashID string `db:"stash_id" json:"stash_id"` + Endpoint string `db:"endpoint" json:"endpoint"` + UpdatedAt *time.Time `db:"updated_at" json:"updated_at"` +} + +func (s StashIDInput) ToStashID() StashID { + ret := StashID{ + StashID: s.StashID, + Endpoint: s.Endpoint, + } + if s.UpdatedAt != nil { + ret.UpdatedAt = *s.UpdatedAt + } else { + // default to now if not provided + ret.UpdatedAt = time.Now() + } + + return ret +} + +type StashIDInputs []StashIDInput + +func (s StashIDInputs) ToStashIDs() StashIDs { + if s == nil { + return nil + } + + ret := make(StashIDs, len(s)) + for i, v := range s { + ret[i] = v.ToStashID() + } + return ret } type UpdateStashIDs struct { diff --git a/pkg/models/studio.go b/pkg/models/studio.go index d5575b7ad3b..03ea8a84dcd 100644 --- a/pkg/models/studio.go +++ b/pkg/models/studio.go @@ -51,14 +51,14 @@ type StudioCreateInput struct { URL *string `json:"url"` ParentID *string `json:"parent_id"` // This should be a URL or a base64 encoded data URL - Image *string `json:"image"` - StashIds []StashID `json:"stash_ids"` - Rating100 *int `json:"rating100"` - Favorite *bool `json:"favorite"` - Details *string `json:"details"` - Aliases []string `json:"aliases"` - TagIds []string `json:"tag_ids"` - IgnoreAutoTag *bool `json:"ignore_auto_tag"` + Image *string `json:"image"` + StashIds []StashIDInput `json:"stash_ids"` + Rating100 *int `json:"rating100"` + Favorite *bool `json:"favorite"` + Details *string `json:"details"` + Aliases []string `json:"aliases"` + TagIds []string `json:"tag_ids"` + IgnoreAutoTag *bool `json:"ignore_auto_tag"` } type StudioUpdateInput struct { @@ -67,12 +67,12 @@ type StudioUpdateInput struct { URL *string `json:"url"` ParentID *string `json:"parent_id"` // This should be a URL or a base64 encoded data URL - Image *string `json:"image"` - StashIds []StashID `json:"stash_ids"` - Rating100 *int `json:"rating100"` - Favorite *bool `json:"favorite"` - Details *string `json:"details"` - Aliases []string `json:"aliases"` - TagIds []string `json:"tag_ids"` - IgnoreAutoTag *bool `json:"ignore_auto_tag"` + Image *string `json:"image"` + StashIds []StashIDInput `json:"stash_ids"` + Rating100 *int `json:"rating100"` + Favorite *bool `json:"favorite"` + Details *string `json:"details"` + Aliases []string `json:"aliases"` + TagIds []string `json:"tag_ids"` + IgnoreAutoTag *bool `json:"ignore_auto_tag"` } diff --git a/pkg/scene/update_test.go b/pkg/scene/update_test.go index 96ebb491f66..f72c9640394 100644 --- a/pkg/scene/update_test.go +++ b/pkg/scene/update_test.go @@ -4,6 +4,7 @@ import ( "errors" "strconv" "testing" + "time" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/mocks" @@ -236,16 +237,19 @@ func TestUpdateSet_UpdateInput(t *testing.T) { tagIDStrs := intslice.IntSliceToStringSlice(tagIDs) stashID := "stashID" endpoint := "endpoint" + updatedAt := time.Now() stashIDs := []models.StashID{ { - StashID: stashID, - Endpoint: endpoint, + StashID: stashID, + Endpoint: endpoint, + UpdatedAt: updatedAt, }, } - stashIDInputs := []models.StashID{ + stashIDInputs := []models.StashIDInput{ { - StashID: stashID, - Endpoint: endpoint, + StashID: stashID, + Endpoint: endpoint, + UpdatedAt: &updatedAt, }, } diff --git a/pkg/sqlite/database.go b/pkg/sqlite/database.go index 0510d7baf26..965c44ef9f4 100644 --- a/pkg/sqlite/database.go +++ b/pkg/sqlite/database.go @@ -34,7 +34,7 @@ const ( cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE" ) -var appSchemaVersion uint = 68 +var appSchemaVersion uint = 69 //go:embed migrations/*.sql var migrationsBox embed.FS diff --git a/pkg/sqlite/migrations/69_stash_id_updated_at.up.sql b/pkg/sqlite/migrations/69_stash_id_updated_at.up.sql new file mode 100644 index 00000000000..1ffb280bd48 --- /dev/null +++ b/pkg/sqlite/migrations/69_stash_id_updated_at.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `performer_stash_ids` ADD COLUMN `updated_at` datetime not null default '1970-01-01T00:00:00Z'; +ALTER TABLE `scene_stash_ids` ADD COLUMN `updated_at` datetime not null default '1970-01-01T00:00:00Z'; +ALTER TABLE `studio_stash_ids` ADD COLUMN `updated_at` datetime not null default '1970-01-01T00:00:00Z'; diff --git a/pkg/sqlite/repository.go b/pkg/sqlite/repository.go index 8eb87b9aff1..2035b11c2fc 100644 --- a/pkg/sqlite/repository.go +++ b/pkg/sqlite/repository.go @@ -14,11 +14,6 @@ import ( const idColumn = "id" -type objectList interface { - Append(o interface{}) - New() interface{} -} - type repository struct { tableName string idColumn string @@ -124,17 +119,6 @@ func (r *repository) queryFunc(ctx context.Context, query string, args []interfa return nil } -func (r *repository) query(ctx context.Context, query string, args []interface{}, out objectList) error { - return r.queryFunc(ctx, query, args, false, func(rows *sqlx.Rows) error { - object := out.New() - if err := rows.StructScan(object); err != nil { - return err - } - out.Append(object) - return nil - }) -} - func (r *repository) queryStruct(ctx context.Context, query string, args []interface{}, out interface{}) error { if err := r.queryFunc(ctx, query, args, true, func(rows *sqlx.Rows) error { if err := rows.StructScan(out); err != nil { @@ -421,7 +405,7 @@ type stashIDRepository struct { type stashIDs []models.StashID func (s *stashIDs) Append(o interface{}) { - *s = append(*s, *o.(*models.StashID)) + *s = append(*s, o.(models.StashID)) } func (s *stashIDs) New() interface{} { @@ -429,10 +413,17 @@ func (s *stashIDs) New() interface{} { } func (r *stashIDRepository) get(ctx context.Context, id int) ([]models.StashID, error) { - query := fmt.Sprintf("SELECT stash_id, endpoint from %s WHERE %s = ?", r.tableName, r.idColumn) + query := fmt.Sprintf("SELECT stash_id, endpoint, updated_at from %s WHERE %s = ?", r.tableName, r.idColumn) var ret stashIDs - err := r.query(ctx, query, []interface{}{id}, &ret) - return []models.StashID(ret), err + err := r.queryFunc(ctx, query, []interface{}{id}, false, func(rows *sqlx.Rows) error { + var v stashIDRow + if err := rows.StructScan(&v); err != nil { + return err + } + ret.Append(v.resolve()) + return nil + }) + return ret, err } type filesRepository struct { diff --git a/pkg/sqlite/table.go b/pkg/sqlite/table.go index 80d6b718a7f..e374f0790e3 100644 --- a/pkg/sqlite/table.go +++ b/pkg/sqlite/table.go @@ -275,19 +275,21 @@ type stashIDTable struct { } type stashIDRow struct { - StashID null.String `db:"stash_id"` - Endpoint null.String `db:"endpoint"` + StashID null.String `db:"stash_id"` + Endpoint null.String `db:"endpoint"` + UpdatedAt Timestamp `db:"updated_at"` } func (r *stashIDRow) resolve() models.StashID { return models.StashID{ - StashID: r.StashID.String, - Endpoint: r.Endpoint.String, + StashID: r.StashID.String, + Endpoint: r.Endpoint.String, + UpdatedAt: r.UpdatedAt.Timestamp, } } func (t *stashIDTable) get(ctx context.Context, id int) ([]models.StashID, error) { - q := dialect.Select("endpoint", "stash_id").From(t.table.table).Where(t.idColumn.Eq(id)) + q := dialect.Select("endpoint", "stash_id", "updated_at").From(t.table.table).Where(t.idColumn.Eq(id)) const single = false var ret []models.StashID @@ -308,8 +310,8 @@ func (t *stashIDTable) get(ctx context.Context, id int) ([]models.StashID, error } func (t *stashIDTable) insertJoin(ctx context.Context, id int, v models.StashID) (sql.Result, error) { - q := dialect.Insert(t.table.table).Cols(t.idColumn.GetCol(), "endpoint", "stash_id").Vals( - goqu.Vals{id, v.Endpoint, v.StashID}, + var q = dialect.Insert(t.table.table).Cols(t.idColumn.GetCol(), "endpoint", "stash_id", "updated_at").Vals( + goqu.Vals{id, v.Endpoint, v.StashID, v.UpdatedAt}, ) ret, err := exec(ctx, q) if err != nil { diff --git a/ui/v2.5/graphql/data/performer-slim.graphql b/ui/v2.5/graphql/data/performer-slim.graphql index 1a4b9833bc8..56a30842ddb 100644 --- a/ui/v2.5/graphql/data/performer-slim.graphql +++ b/ui/v2.5/graphql/data/performer-slim.graphql @@ -27,6 +27,7 @@ fragment SlimPerformerData on Performer { stash_ids { endpoint stash_id + updated_at } rating100 death_date diff --git a/ui/v2.5/graphql/data/performer.graphql b/ui/v2.5/graphql/data/performer.graphql index 144382a4522..0aa60ce21bb 100644 --- a/ui/v2.5/graphql/data/performer.graphql +++ b/ui/v2.5/graphql/data/performer.graphql @@ -34,6 +34,7 @@ fragment PerformerData on Performer { stash_ids { stash_id endpoint + updated_at } rating100 details diff --git a/ui/v2.5/graphql/data/scene-slim.graphql b/ui/v2.5/graphql/data/scene-slim.graphql index 7e2a4ffad2d..d5899a24764 100644 --- a/ui/v2.5/graphql/data/scene-slim.graphql +++ b/ui/v2.5/graphql/data/scene-slim.graphql @@ -84,5 +84,6 @@ fragment SlimSceneData on Scene { stash_ids { endpoint stash_id + updated_at } } diff --git a/ui/v2.5/graphql/data/scene.graphql b/ui/v2.5/graphql/data/scene.graphql index ef58922295a..e4a6e5cc69f 100644 --- a/ui/v2.5/graphql/data/scene.graphql +++ b/ui/v2.5/graphql/data/scene.graphql @@ -71,6 +71,7 @@ fragment SceneData on Scene { stash_ids { endpoint stash_id + updated_at } sceneStreams { diff --git a/ui/v2.5/graphql/data/studio-slim.graphql b/ui/v2.5/graphql/data/studio-slim.graphql index 406a2ffa70a..cf101bd047c 100644 --- a/ui/v2.5/graphql/data/studio-slim.graphql +++ b/ui/v2.5/graphql/data/studio-slim.graphql @@ -5,6 +5,7 @@ fragment SlimStudioData on Studio { stash_ids { endpoint stash_id + updated_at } parent_studio { id diff --git a/ui/v2.5/graphql/data/studio.graphql b/ui/v2.5/graphql/data/studio.graphql index feb35136fed..25e77675549 100644 --- a/ui/v2.5/graphql/data/studio.graphql +++ b/ui/v2.5/graphql/data/studio.graphql @@ -28,6 +28,7 @@ fragment StudioData on Studio { stash_ids { stash_id endpoint + updated_at } details rating100 diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index e7d7a8b41e3..2adcb601e1e 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -282,7 +282,10 @@ export const PerformerEditPanel: React.FC = ({ formik.setFieldValue("penis_length", state.penis_length); } - const remoteSiteID = state.remote_site_id; + updateStashIDs(state.remote_site_id); + } + + function updateStashIDs(remoteSiteID: string | null | undefined) { if (remoteSiteID && (scraper as IStashBox).endpoint) { const newIDs = formik.values.stash_ids?.filter( @@ -291,6 +294,7 @@ export const PerformerEditPanel: React.FC = ({ newIDs?.push({ endpoint: (scraper as IStashBox).endpoint, stash_id: remoteSiteID, + updated_at: new Date().toISOString(), }); formik.setFieldValue("stash_ids", newIDs); } @@ -438,6 +442,7 @@ export const PerformerEditPanel: React.FC = ({ setScraper(undefined); } else { setScrapedPerformer(result); + updateStashIDs(performerResult.remote_site_id); } } diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index 2eef3de1fb4..fb91cd44925 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -508,6 +508,7 @@ export const SceneEditPanel: React.FC = ({ return { endpoint, stash_id: updatedScene.remote_site_id, + updated_at: new Date().toISOString(), }; } @@ -521,6 +522,7 @@ export const SceneEditPanel: React.FC = ({ formik.values.stash_ids.concat({ endpoint, stash_id: updatedScene.remote_site_id, + updated_at: new Date().toISOString(), }) ); } diff --git a/ui/v2.5/src/components/Shared/StashID.tsx b/ui/v2.5/src/components/Shared/StashID.tsx index 14bcef6882c..00bddf58edf 100644 --- a/ui/v2.5/src/components/Shared/StashID.tsx +++ b/ui/v2.5/src/components/Shared/StashID.tsx @@ -7,7 +7,7 @@ import { ExternalLink } from "./ExternalLink"; export type LinkType = "performers" | "scenes" | "studios"; export const StashIDPill: React.FC<{ - stashID: StashId; + stashID: Pick; linkType: LinkType; }> = ({ stashID, linkType }) => { const { configuration } = React.useContext(ConfigurationContext); diff --git a/ui/v2.5/src/components/Tagger/PerformerModal.tsx b/ui/v2.5/src/components/Tagger/PerformerModal.tsx index 37a2009aa76..d4c8bba1636 100755 --- a/ui/v2.5/src/components/Tagger/PerformerModal.tsx +++ b/ui/v2.5/src/components/Tagger/PerformerModal.tsx @@ -272,6 +272,7 @@ const PerformerModal: React.FC = ({ { endpoint, stash_id: remoteSiteID, + updated_at: new Date().toISOString(), }, ]; } diff --git a/ui/v2.5/src/components/Tagger/context.tsx b/ui/v2.5/src/components/Tagger/context.tsx index dc35208c5f2..9b1e996de24 100644 --- a/ui/v2.5/src/components/Tagger/context.tsx +++ b/ui/v2.5/src/components/Tagger/context.tsx @@ -613,12 +613,14 @@ export const TaggerContext: React.FC = ({ children }) => { return { endpoint: e.endpoint, stash_id: e.stash_id, + updated_at: e.updated_at, }; }); stashIDs.push({ stash_id: performer.remote_site_id, endpoint: currentSource?.sourceInput.stash_box_endpoint, + updated_at: new Date().toISOString(), }); await updatePerformer({ @@ -770,12 +772,14 @@ export const TaggerContext: React.FC = ({ children }) => { return { endpoint: e.endpoint, stash_id: e.stash_id, + updated_at: e.updated_at, }; }); stashIDs.push({ stash_id: studio.remote_site_id, endpoint: currentSource?.sourceInput.stash_box_endpoint, + updated_at: new Date().toISOString(), }); await updateStudio({ diff --git a/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx b/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx index cc8f6a132e6..4be285907b6 100755 --- a/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx @@ -413,6 +413,7 @@ const StashSearchResult: React.FC = ({ return { endpoint: s.endpoint, stash_id: s.stash_id, + updated_at: s.updated_at, }; }) .filter( @@ -421,6 +422,7 @@ const StashSearchResult: React.FC = ({ { endpoint: currentSource.sourceInput.stash_box_endpoint, stash_id: scene.remote_site_id, + updated_at: new Date().toISOString(), }, ]; } else { diff --git a/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx b/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx index b57c796ab28..249e34e7401 100644 --- a/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx @@ -198,11 +198,13 @@ const StudioModal: React.FC = ({ // stashid handling code const remoteSiteID = studio.remote_site_id; + const timeNow = new Date().toISOString(); if (remoteSiteID && endpoint) { studioData.stash_ids = [ { endpoint, stash_id: remoteSiteID, + updated_at: timeNow, }, ]; } @@ -230,6 +232,7 @@ const StudioModal: React.FC = ({ { endpoint, stash_id: parentRemoteSiteID, + updated_at: timeNow, }, ]; } diff --git a/ui/v2.5/src/utils/stashIds.ts b/ui/v2.5/src/utils/stashIds.ts index 3240b2a0f56..289ce9c9d70 100644 --- a/ui/v2.5/src/utils/stashIds.ts +++ b/ui/v2.5/src/utils/stashIds.ts @@ -1,5 +1,8 @@ -export const getStashIDs = (ids?: { stash_id: string; endpoint: string }[]) => - (ids ?? []).map(({ stash_id, endpoint }) => ({ +export const getStashIDs = ( + ids?: { stash_id: string; endpoint: string; updated_at: string }[] +) => + (ids ?? []).map(({ stash_id, endpoint, updated_at }) => ({ stash_id, endpoint, + updated_at, }));