From 84adacaebaf65e10bce5cec61f776f6b67b753d3 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 2 Nov 2022 22:25:15 +0000 Subject: [PATCH 01/24] Refactor performer relationships --- internal/api/resolver_model_performer.go | 23 +- internal/api/resolver_mutation_performer.go | 78 ++-- internal/api/resolver_mutation_scene.go | 50 --- internal/identify/performer.go | 17 +- internal/identify/performer_test.go | 26 -- internal/manager/task_stash_box_tag.go | 40 +- pkg/models/model_performer.go | 31 ++ pkg/models/performer.go | 4 +- pkg/performer/export_test.go | 4 + pkg/performer/import.go | 27 +- pkg/performer/import_test.go | 30 +- pkg/sqlite/performer.go | 48 ++- pkg/sqlite/performer_test.go | 382 +++++++++++++++++++- pkg/sqlite/record.go | 2 +- pkg/sqlite/setup_test.go | 53 +-- pkg/sqlite/tables.go | 15 + 16 files changed, 545 insertions(+), 285 deletions(-) diff --git a/internal/api/resolver_model_performer.go b/internal/api/resolver_model_performer.go index f80cd4c3585..807f5294f13 100644 --- a/internal/api/resolver_model_performer.go +++ b/internal/api/resolver_model_performer.go @@ -4,6 +4,7 @@ import ( "context" "strconv" + "github.com/stashapp/stash/internal/api/loaders" "github.com/stashapp/stash/internal/api/urlbuilders" "github.com/stashapp/stash/pkg/gallery" "github.com/stashapp/stash/pkg/image" @@ -37,14 +38,17 @@ func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer } func (r *performerResolver) Tags(ctx context.Context, obj *models.Performer) (ret []*models.Tag, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { - ret, err = r.repository.Tag.FindByPerformerID(ctx, obj.ID) - return err - }); err != nil { - return nil, err + if !obj.TagIDs.Loaded() { + if err := r.withTxn(ctx, func(ctx context.Context) error { + return obj.LoadTagIDs(ctx, r.repository.Performer) + }); err != nil { + return nil, err + } } - return ret, nil + var errs []error + ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List()) + return ret, firstError(errs) } func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performer) (ret *int, err error) { @@ -95,16 +99,13 @@ func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) ( } func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) ([]*models.StashID, error) { - var ret []models.StashID if err := r.withTxn(ctx, func(ctx context.Context) error { - var err error - ret, err = r.repository.Performer.GetStashIDs(ctx, obj.ID) - return err + return obj.LoadStashIDs(ctx, r.repository.Performer) }); err != nil { return nil, err } - return stashIDsSliceToPtrSlice(ret), nil + return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil } func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer) (*string, error) { diff --git a/internal/api/resolver_mutation_performer.go b/internal/api/resolver_mutation_performer.go index 15c6610a8a1..44d3293047a 100644 --- a/internal/api/resolver_mutation_performer.go +++ b/internal/api/resolver_mutation_performer.go @@ -50,11 +50,18 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC return nil, err } + tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds) + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) + } + // Populate a new performer from the input currentTime := time.Now() newPerformer := models.Performer{ Name: input.Name, Checksum: checksum, + TagIDs: models.NewRelatedIDs(tagIDs), + StashIDs: models.NewRelatedStashIDs(stashIDPtrSliceToSlice(input.StashIds)), CreatedAt: currentTime, UpdatedAt: currentTime, } @@ -149,12 +156,6 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC return err } - if len(input.TagIds) > 0 { - if err := r.updatePerformerTags(ctx, newPerformer.ID, input.TagIds); err != nil { - return err - } - } - // update image table if len(imageData) > 0 { if err := qb.UpdateImage(ctx, newPerformer.ID, imageData); err != nil { @@ -162,14 +163,6 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC } } - // Save the stash_ids - if input.StashIds != nil { - stashIDJoins := stashIDPtrSliceToSlice(input.StashIds) - if err := qb.UpdateStashIDs(ctx, newPerformer.ID, stashIDJoins); err != nil { - return err - } - } - return nil }); err != nil { return nil, err @@ -246,6 +239,21 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight") updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") + if translator.hasField("tag_ids") { + updatedPerformer.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet) + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) + } + } + + // Save the stash_ids + if translator.hasField("stash_ids") { + updatedPerformer.StashIDs = &models.UpdateStashIDs{ + StashIDs: stashIDPtrSliceToSlice(input.StashIds), + Mode: models.RelationshipUpdateModeSet, + } + } + // Start the transaction and save the p if err := r.withTxn(ctx, func(ctx context.Context) error { qb := r.repository.Performer @@ -271,13 +279,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU return err } - // Save the tags - if translator.hasField("tag_ids") { - if err := r.updatePerformerTags(ctx, performerID, input.TagIds); err != nil { - return err - } - } - // update image table if len(imageData) > 0 { if err := qb.UpdateImage(ctx, performerID, imageData); err != nil { @@ -290,14 +291,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU } } - // Save the stash_ids - if translator.hasField("stash_ids") { - stashIDJoins := stashIDPtrSliceToSlice(input.StashIds) - if err := qb.UpdateStashIDs(ctx, performerID, stashIDJoins); err != nil { - return err - } - } - return nil }); err != nil { return nil, err @@ -307,14 +300,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU return r.getPerformer(ctx, performerID) } -func (r *mutationResolver) updatePerformerTags(ctx context.Context, performerID int, tagsIDs []string) error { - ids, err := stringslice.StringSliceToIntSlice(tagsIDs) - if err != nil { - return err - } - return r.repository.Performer.UpdateTags(ctx, performerID, ids) -} - func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPerformerUpdateInput) ([]*models.Performer, error) { performerIDs, err := stringslice.StringSliceToIntSlice(input.Ids) if err != nil { @@ -367,6 +352,13 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe } } + if translator.hasField("tag_ids") { + updatedPerformer.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode) + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) + } + } + ret := []*models.Performer{} // Start the transaction and save the scene marker @@ -396,18 +388,6 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe } ret = append(ret, performer) - - // Save the tags - if translator.hasField("tag_ids") { - tagIDs, err := adjustTagIDs(ctx, qb, performerID, *input.TagIds) - if err != nil { - return err - } - - if err := qb.UpdateTags(ctx, performerID, tagIDs); err != nil { - return err - } - } } return nil diff --git a/internal/api/resolver_mutation_scene.go b/internal/api/resolver_mutation_scene.go index ff1981ef56a..ac382242fa2 100644 --- a/internal/api/resolver_mutation_scene.go +++ b/internal/api/resolver_mutation_scene.go @@ -326,56 +326,6 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU return newRet, nil } -func adjustIDs(existingIDs []int, updateIDs BulkUpdateIds) []int { - // if we are setting the ids, just return the ids - if updateIDs.Mode == models.RelationshipUpdateModeSet { - existingIDs = []int{} - for _, idStr := range updateIDs.Ids { - id, _ := strconv.Atoi(idStr) - existingIDs = append(existingIDs, id) - } - - return existingIDs - } - - for _, idStr := range updateIDs.Ids { - id, _ := strconv.Atoi(idStr) - - // look for the id in the list - foundExisting := false - for idx, existingID := range existingIDs { - if existingID == id { - if updateIDs.Mode == models.RelationshipUpdateModeRemove { - // remove from the list - existingIDs = append(existingIDs[:idx], existingIDs[idx+1:]...) - } - - foundExisting = true - break - } - } - - if !foundExisting && updateIDs.Mode != models.RelationshipUpdateModeRemove { - existingIDs = append(existingIDs, id) - } - } - - return existingIDs -} - -type tagIDsGetter interface { - GetTagIDs(ctx context.Context, id int) ([]int, error) -} - -func adjustTagIDs(ctx context.Context, qb tagIDsGetter, sceneID int, ids BulkUpdateIds) (ret []int, err error) { - ret, err = qb.GetTagIDs(ctx, sceneID) - if err != nil { - return nil, err - } - - return adjustIDs(ret, ids), nil -} - func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) { sceneID, err := strconv.Atoi(input.ID) if err != nil { diff --git a/internal/identify/performer.go b/internal/identify/performer.go index d417d8bac81..5108905a35d 100644 --- a/internal/identify/performer.go +++ b/internal/identify/performer.go @@ -12,7 +12,6 @@ import ( type PerformerCreator interface { Create(ctx context.Context, newPerformer *models.Performer) error - UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error } func getPerformerID(ctx context.Context, endpoint string, w PerformerCreator, p *models.ScrapedPerformer, createMissing bool) (*int, error) { @@ -33,20 +32,18 @@ func getPerformerID(ctx context.Context, endpoint string, w PerformerCreator, p func createMissingPerformer(ctx context.Context, endpoint string, w PerformerCreator, p *models.ScrapedPerformer) (*int, error) { performerInput := scrapedToPerformerInput(p) - err := w.Create(ctx, &performerInput) - if err != nil { - return nil, fmt.Errorf("error creating performer: %w", err) - } - if endpoint != "" && p.RemoteSiteID != nil { - if err := w.UpdateStashIDs(ctx, performerInput.ID, []models.StashID{ + performerInput.StashIDs = models.NewRelatedStashIDs([]models.StashID{ { Endpoint: endpoint, StashID: *p.RemoteSiteID, }, - }); err != nil { - return nil, fmt.Errorf("error setting performer stash id: %w", err) - } + }) + } + + err := w.Create(ctx, &performerInput) + if err != nil { + return nil, fmt.Errorf("error creating performer: %w", err) } return &performerInput.ID, nil diff --git a/internal/identify/performer_test.go b/internal/identify/performer_test.go index 764b4ec79ef..7e71bdcdb32 100644 --- a/internal/identify/performer_test.go +++ b/internal/identify/performer_test.go @@ -127,7 +127,6 @@ func Test_getPerformerID(t *testing.T) { func Test_createMissingPerformer(t *testing.T) { emptyEndpoint := "" validEndpoint := "validEndpoint" - invalidEndpoint := "invalidEndpoint" remoteSiteID := "remoteSiteID" validName := "validName" invalidName := "invalidName" @@ -145,19 +144,6 @@ func Test_createMissingPerformer(t *testing.T) { return p.Name == invalidName })).Return(errors.New("error creating performer")) - mockPerformerReaderWriter.On("UpdateStashIDs", testCtx, performerID, []models.StashID{ - { - Endpoint: invalidEndpoint, - StashID: remoteSiteID, - }, - }).Return(errors.New("error updating stash ids")) - mockPerformerReaderWriter.On("UpdateStashIDs", testCtx, performerID, []models.StashID{ - { - Endpoint: validEndpoint, - StashID: remoteSiteID, - }, - }).Return(nil) - type args struct { endpoint string p *models.ScrapedPerformer @@ -202,18 +188,6 @@ func Test_createMissingPerformer(t *testing.T) { &performerID, false, }, - { - "invalid stash id", - args{ - invalidEndpoint, - &models.ScrapedPerformer{ - Name: &validName, - RemoteSiteID: &remoteSiteID, - }, - }, - nil, - true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/manager/task_stash_box_tag.go b/internal/manager/task_stash_box_tag.go index 33b26d6895d..ba7f0e8d37f 100644 --- a/internal/manager/task_stash_box_tag.go +++ b/internal/manager/task_stash_box_tag.go @@ -149,22 +149,21 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { if performer.URL != nil && !excluded["url"] { partial.URL = models.NewOptionalString(*performer.URL) } - - txnErr := txn.WithTxn(ctx, instance.Repository, func(ctx context.Context) error { - r := instance.Repository - _, err := r.Performer.UpdatePartial(ctx, t.performer.ID, partial) - - if !t.refresh { - err = r.Performer.UpdateStashIDs(ctx, t.performer.ID, []models.StashID{ + if !t.refresh { + partial.StashIDs = &models.UpdateStashIDs{ + StashIDs: []models.StashID{ { Endpoint: t.box.Endpoint, StashID: *performer.RemoteSiteID, }, - }) - if err != nil { - return err - } + }, + Mode: models.RelationshipUpdateModeSet, } + } + + txnErr := txn.WithTxn(ctx, instance.Repository, func(ctx context.Context) error { + r := instance.Repository + _, err := r.Performer.UpdatePartial(ctx, t.performer.ID, partial) if len(performer.Images) > 0 && !excluded["image"] { image, err := utils.ReadImageFromURL(ctx, performer.Images[0]) @@ -211,21 +210,18 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { Tattoos: getString(performer.Tattoos), Twitter: getString(performer.Twitter), URL: getString(performer.URL), - UpdatedAt: currentTime, - } - err := txn.WithTxn(ctx, instance.Repository, func(ctx context.Context) error { - r := instance.Repository - err := r.Performer.Create(ctx, &newPerformer) - if err != nil { - return err - } - - err = r.Performer.UpdateStashIDs(ctx, newPerformer.ID, []models.StashID{ + StashIDs: models.NewRelatedStashIDs([]models.StashID{ { Endpoint: t.box.Endpoint, StashID: *performer.RemoteSiteID, }, - }) + }), + UpdatedAt: currentTime, + } + + err := txn.WithTxn(ctx, instance.Repository, func(ctx context.Context) error { + r := instance.Repository + err := r.Performer.Create(ctx, &newPerformer) if err != nil { return err } diff --git a/pkg/models/model_performer.go b/pkg/models/model_performer.go index b6c9eff4d20..d11481b99af 100644 --- a/pkg/models/model_performer.go +++ b/pkg/models/model_performer.go @@ -1,6 +1,7 @@ package models import ( + "context" "time" "github.com/stashapp/stash/pkg/hash/md5" @@ -34,6 +35,33 @@ type Performer struct { HairColor string `json:"hair_color"` Weight *int `json:"weight"` IgnoreAutoTag bool `json:"ignore_auto_tag"` + + TagIDs RelatedIDs `json:"tag_ids"` + StashIDs RelatedStashIDs `json:"stash_ids"` +} + +func (s *Performer) LoadTagIDs(ctx context.Context, l TagIDLoader) error { + return s.TagIDs.load(func() ([]int, error) { + return l.GetTagIDs(ctx, s.ID) + }) +} + +func (s *Performer) LoadStashIDs(ctx context.Context, l StashIDLoader) error { + return s.StashIDs.load(func() ([]StashID, error) { + return l.GetStashIDs(ctx, s.ID) + }) +} + +func (s *Performer) LoadRelationships(ctx context.Context, l PerformerReader) error { + if err := s.LoadTagIDs(ctx, l); err != nil { + return err + } + + if err := s.LoadStashIDs(ctx, l); err != nil { + return err + } + + return nil } // PerformerPartial represents part of a Performer object. It is used to update @@ -66,6 +94,9 @@ type PerformerPartial struct { HairColor OptionalString Weight OptionalInt IgnoreAutoTag OptionalBool + + TagIDs *UpdateIDs + StashIDs *UpdateStashIDs } func NewPerformer(name string) *Performer { diff --git a/pkg/models/performer.go b/pkg/models/performer.go index 191f6b4b9f9..b7d8c72dc20 100644 --- a/pkg/models/performer.go +++ b/pkg/models/performer.go @@ -149,7 +149,7 @@ type PerformerReader interface { Query(ctx context.Context, performerFilter *PerformerFilterType, findFilter *FindFilterType) ([]*Performer, int, error) GetImage(ctx context.Context, performerID int) ([]byte, error) StashIDLoader - GetTagIDs(ctx context.Context, performerID int) ([]int, error) + TagIDLoader } type PerformerWriter interface { @@ -159,8 +159,6 @@ type PerformerWriter interface { Destroy(ctx context.Context, id int) error UpdateImage(ctx context.Context, performerID int, image []byte) error DestroyImage(ctx context.Context, performerID int) error - UpdateStashIDs(ctx context.Context, performerID int, stashIDs []StashID) error - UpdateTags(ctx context.Context, performerID int, tagIDs []int) error } type PerformerReaderWriter interface { diff --git a/pkg/performer/export_test.go b/pkg/performer/export_test.go index d3ee15d46cb..e41b5509fff 100644 --- a/pkg/performer/export_test.go +++ b/pkg/performer/export_test.go @@ -97,6 +97,10 @@ func createFullPerformer(id int, name string) *models.Performer { HairColor: hairColor, Weight: &weight, IgnoreAutoTag: autoTagIgnored, + TagIDs: models.NewRelatedIDs([]int{}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{ + stashID, + }), } } diff --git a/pkg/performer/import.go b/pkg/performer/import.go index 62c1d1b9526..76e4ab09fda 100644 --- a/pkg/performer/import.go +++ b/pkg/performer/import.go @@ -18,9 +18,7 @@ import ( type NameFinderCreatorUpdater interface { NameFinderCreator Update(ctx context.Context, updatedPerformer *models.Performer) error - UpdateTags(ctx context.Context, performerID int, tagIDs []int) error UpdateImage(ctx context.Context, performerID int, image []byte) error - UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error } type Importer struct { @@ -32,8 +30,6 @@ type Importer struct { ID int performer models.Performer imageData []byte - - tags []*models.Tag } func (i *Importer) PreImport(ctx context.Context) error { @@ -62,7 +58,9 @@ func (i *Importer) populateTags(ctx context.Context) error { return err } - i.tags = tags + for _, p := range tags { + i.performer.TagIDs.Add(p.ID) + } } return nil @@ -120,28 +118,12 @@ func createTags(ctx context.Context, tagWriter tag.NameFinderCreator, names []st } func (i *Importer) PostImport(ctx context.Context, id int) error { - if len(i.tags) > 0 { - var tagIDs []int - for _, t := range i.tags { - tagIDs = append(tagIDs, t.ID) - } - if err := i.ReaderWriter.UpdateTags(ctx, id, tagIDs); err != nil { - return fmt.Errorf("failed to associate tags: %v", err) - } - } - if len(i.imageData) > 0 { if err := i.ReaderWriter.UpdateImage(ctx, id, i.imageData); err != nil { return fmt.Errorf("error setting performer image: %v", err) } } - if len(i.Input.StashIDs) > 0 { - if err := i.ReaderWriter.UpdateStashIDs(ctx, id, i.Input.StashIDs); err != nil { - return fmt.Errorf("error setting stash id: %v", err) - } - } - return nil } @@ -210,6 +192,9 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform IgnoreAutoTag: performerJSON.IgnoreAutoTag, CreatedAt: performerJSON.CreatedAt.GetTime(), UpdatedAt: performerJSON.UpdatedAt.GetTime(), + + TagIDs: models.NewRelatedIDs([]int{}), + StashIDs: models.NewRelatedStashIDs(performerJSON.StashIDs), } if performerJSON.Birthdate != "" { diff --git a/pkg/performer/import_test.go b/pkg/performer/import_test.go index 08f2c5b0ca6..41da960003c 100644 --- a/pkg/performer/import_test.go +++ b/pkg/performer/import_test.go @@ -87,7 +87,7 @@ func TestImporterPreImportWithTag(t *testing.T) { err := i.PreImport(testCtx) assert.Nil(t, err) - assert.Equal(t, existingTagID, i.tags[0].ID) + assert.Equal(t, existingTagID, i.performer.TagIDs.List()[0]) i.Input.Tags = []string{existingTagErr} err = i.PreImport(testCtx) @@ -124,7 +124,7 @@ func TestImporterPreImportWithMissingTag(t *testing.T) { i.MissingRefBehaviour = models.ImportMissingRefEnumCreate err = i.PreImport(testCtx) assert.Nil(t, err) - assert.Equal(t, existingTagID, i.tags[0].ID) + assert.Equal(t, existingTagID, i.performer.TagIDs.List()[0]) tagReaderWriter.AssertExpectations(t) } @@ -207,32 +207,6 @@ func TestImporterFindExistingID(t *testing.T) { readerWriter.AssertExpectations(t) } -func TestImporterPostImportUpdateTags(t *testing.T) { - readerWriter := &mocks.PerformerReaderWriter{} - - i := Importer{ - ReaderWriter: readerWriter, - tags: []*models.Tag{ - { - ID: existingTagID, - }, - }, - } - - updateErr := errors.New("UpdateTags error") - - readerWriter.On("UpdateTags", testCtx, performerID, []int{existingTagID}).Return(nil).Once() - readerWriter.On("UpdateTags", testCtx, errTagsID, mock.AnythingOfType("[]int")).Return(updateErr).Once() - - err := i.PostImport(testCtx, performerID) - assert.Nil(t, err) - - err = i.PostImport(testCtx, errTagsID) - assert.NotNil(t, err) - - readerWriter.AssertExpectations(t) -} - func TestCreate(t *testing.T) { readerWriter := &mocks.PerformerReaderWriter{} diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index c0e7ea70085..38a68a3ec68 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -180,6 +180,18 @@ func (qb *PerformerStore) Create(ctx context.Context, newObject *models.Performe return err } + if newObject.TagIDs.Loaded() { + if err := performersTagsTableMgr.insertJoins(ctx, id, newObject.TagIDs.List()); err != nil { + return err + } + } + + if newObject.StashIDs.Loaded() { + if err := performersStashIDsTableMgr.insertJoins(ctx, id, newObject.StashIDs.List()); err != nil { + return err + } + } + updated, err := qb.Find(ctx, id) if err != nil { return fmt.Errorf("finding after create: %w", err) @@ -190,14 +202,14 @@ func (qb *PerformerStore) Create(ctx context.Context, newObject *models.Performe return nil } -func (qb *PerformerStore) UpdatePartial(ctx context.Context, id int, updatedObject models.PerformerPartial) (*models.Performer, error) { +func (qb *PerformerStore) UpdatePartial(ctx context.Context, id int, partial models.PerformerPartial) (*models.Performer, error) { r := performerRowRecord{ updateRecord{ Record: make(exp.Record), }, } - r.fromPartial(updatedObject) + r.fromPartial(partial) if len(r.Record) > 0 { if err := qb.tableMgr.updateByID(ctx, id, r.Record); err != nil { @@ -205,6 +217,17 @@ func (qb *PerformerStore) UpdatePartial(ctx context.Context, id int, updatedObje } } + if partial.TagIDs != nil { + if err := performersTagsTableMgr.modifyJoins(ctx, id, partial.TagIDs.IDs, partial.TagIDs.Mode); err != nil { + return nil, err + } + } + if partial.StashIDs != nil { + if err := performersStashIDsTableMgr.modifyJoins(ctx, id, partial.StashIDs.StashIDs, partial.StashIDs.Mode); err != nil { + return nil, err + } + } + return qb.Find(ctx, id) } @@ -216,6 +239,18 @@ func (qb *PerformerStore) Update(ctx context.Context, updatedObject *models.Perf return err } + if updatedObject.TagIDs.Loaded() { + if err := performersTagsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.TagIDs.List()); err != nil { + return err + } + } + + if updatedObject.StashIDs.Loaded() { + if err := performersStashIDsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.StashIDs.List()); err != nil { + return err + } + } + return nil } @@ -799,11 +834,6 @@ func (qb *PerformerStore) GetTagIDs(ctx context.Context, id int) ([]int, error) return qb.tagsRepository().getIDs(ctx, id) } -func (qb *PerformerStore) UpdateTags(ctx context.Context, id int, tagIDs []int) error { - // Delete the existing joins and then create new ones - return qb.tagsRepository().replace(ctx, id, tagIDs) -} - func (qb *PerformerStore) imageRepository() *imageRepository { return &imageRepository{ repository: repository{ @@ -841,10 +871,6 @@ func (qb *PerformerStore) GetStashIDs(ctx context.Context, performerID int) ([]m return qb.stashIDRepository().get(ctx, performerID) } -func (qb *PerformerStore) UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error { - return qb.stashIDRepository().replace(ctx, performerID, stashIDs) -} - func (qb *PerformerStore) FindByStashID(ctx context.Context, stashID models.StashID) ([]*models.Performer, error) { sq := dialect.From(performersStashIDsJoinTable).Select(performersStashIDsJoinTable.Col(performerIDColumn)).Where( performersStashIDsJoinTable.Col("stash_id").Eq(stashID.StashID), diff --git a/pkg/sqlite/performer_test.go b/pkg/sqlite/performer_test.go index 2b089a3ef14..99ecd8e641b 100644 --- a/pkg/sqlite/performer_test.go +++ b/pkg/sqlite/performer_test.go @@ -17,6 +17,166 @@ import ( "github.com/stretchr/testify/assert" ) +func loadPerformerRelationships(ctx context.Context, expected models.Performer, actual *models.Performer) error { + if expected.TagIDs.Loaded() { + if err := actual.LoadTagIDs(ctx, db.Performer); err != nil { + return err + } + } + if expected.StashIDs.Loaded() { + if err := actual.LoadStashIDs(ctx, db.Performer); err != nil { + return err + } + } + + return nil +} + +func Test_PerformerStore_Create(t *testing.T) { + var ( + name = "name" + gender = models.GenderEnumFemale + checksum = "checksum" + details = "details" + url = "url" + twitter = "twitter" + instagram = "instagram" + rating = 3 + ethnicity = "ethnicity" + country = "country" + eyeColor = "eyeColor" + height = 134 + measurements = "measurements" + fakeTits = "fakeTits" + careerLength = "careerLength" + tattoos = "tattoos" + piercings = "piercings" + aliases = "aliases" + hairColor = "hairColor" + weight = 123 + ignoreAutoTag = true + favorite = true + endpoint1 = "endpoint1" + endpoint2 = "endpoint2" + stashID1 = "stashid1" + stashID2 = "stashid2" + createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + + birthdate = models.NewDate("2003-02-01") + deathdate = models.NewDate("2023-02-01") + ) + + tests := []struct { + name string + newObject models.Performer + wantErr bool + }{ + { + "full", + models.Performer{ + Name: name, + Checksum: checksum, + Gender: gender, + URL: url, + Twitter: twitter, + Instagram: instagram, + Birthdate: &birthdate, + Ethnicity: ethnicity, + Country: country, + EyeColor: eyeColor, + Height: &height, + Measurements: measurements, + FakeTits: fakeTits, + CareerLength: careerLength, + Tattoos: tattoos, + Piercings: piercings, + Aliases: aliases, + Favorite: favorite, + Rating: &rating, + Details: details, + DeathDate: &deathdate, + HairColor: hairColor, + Weight: &weight, + IgnoreAutoTag: ignoreAutoTag, + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{ + { + StashID: stashID1, + Endpoint: endpoint1, + }, + { + StashID: stashID2, + Endpoint: endpoint2, + }, + }), + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, + false, + }, + { + "invalid tag id", + models.Performer{ + Name: name, + Checksum: checksum, + TagIDs: models.NewRelatedIDs([]int{invalidID}), + }, + true, + }, + } + + qb := db.Performer + + for _, tt := range tests { + runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { + assert := assert.New(t) + + p := tt.newObject + if err := qb.Create(ctx, &p); (err != nil) != tt.wantErr { + t.Errorf("PerformerStore.Create() error = %v, wantErr = %v", err, tt.wantErr) + } + + if tt.wantErr { + assert.Zero(p.ID) + return + } + + assert.NotZero(p.ID) + + copy := tt.newObject + copy.ID = p.ID + + // load relationships + if err := loadPerformerRelationships(ctx, copy, &p); err != nil { + t.Errorf("loadPerformerRelationships() error = %v", err) + return + } + + assert.Equal(copy, p) + + // ensure can find the performer + found, err := qb.Find(ctx, p.ID) + if err != nil { + t.Errorf("PerformerStore.Find() error = %v", err) + } + + if !assert.NotNil(found) { + return + } + + // load relationships + if err := loadPerformerRelationships(ctx, copy, found); err != nil { + t.Errorf("loadPerformerRelationships() error = %v", err) + return + } + assert.Equal(copy, *found) + + return + }) + } +} + func Test_PerformerStore_Update(t *testing.T) { var ( name = "name" @@ -41,6 +201,10 @@ func Test_PerformerStore_Update(t *testing.T) { weight = 123 ignoreAutoTag = true favorite = true + endpoint1 = "endpoint1" + endpoint2 = "endpoint2" + stashID1 = "stashid1" + stashID2 = "stashid2" createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) @@ -81,18 +245,47 @@ func Test_PerformerStore_Update(t *testing.T) { HairColor: hairColor, Weight: &weight, IgnoreAutoTag: ignoreAutoTag, - CreatedAt: createdAt, - UpdatedAt: updatedAt, + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{ + { + StashID: stashID1, + Endpoint: endpoint1, + }, + { + StashID: stashID2, + Endpoint: endpoint2, + }, + }), + CreatedAt: createdAt, + UpdatedAt: updatedAt, }, false, }, { - "clear all", + "clear nullables", &models.Performer{ - ID: performerIDs[performerIdxWithGallery], + ID: performerIDs[performerIdxWithGallery], + TagIDs: models.NewRelatedIDs([]int{}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{}), }, false, }, + { + "clear tag ids", + &models.Performer{ + ID: performerIDs[sceneIdxWithTag], + TagIDs: models.NewRelatedIDs([]int{}), + }, + false, + }, + { + "invalid tag id", + &models.Performer{ + ID: performerIDs[sceneIdxWithGallery], + TagIDs: models.NewRelatedIDs([]int{invalidID}), + }, + true, + }, } qb := db.Performer @@ -115,11 +308,49 @@ func Test_PerformerStore_Update(t *testing.T) { t.Errorf("PerformerStore.Find() error = %v", err) } + // load relationships + if err := loadPerformerRelationships(ctx, copy, s); err != nil { + t.Errorf("loadPerformerRelationships() error = %v", err) + return + } + assert.Equal(copy, *s) }) } } +func clearPerformerPartial() models.PerformerPartial { + nullString := models.OptionalString{Set: true, Null: true} + nullDate := models.OptionalDate{Set: true, Null: true} + nullInt := models.OptionalInt{Set: true, Null: true} + + // leave mandatory fields + return models.PerformerPartial{ + Gender: nullString, + URL: nullString, + Twitter: nullString, + Instagram: nullString, + Birthdate: nullDate, + Ethnicity: nullString, + Country: nullString, + EyeColor: nullString, + Height: nullInt, + Measurements: nullString, + FakeTits: nullString, + CareerLength: nullString, + Tattoos: nullString, + Piercings: nullString, + Aliases: nullString, + Rating: nullInt, + Details: nullString, + DeathDate: nullDate, + HairColor: nullString, + Weight: nullInt, + TagIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, + StashIDs: &models.UpdateStashIDs{Mode: models.RelationshipUpdateModeSet}, + } +} + func Test_PerformerStore_UpdatePartial(t *testing.T) { var ( name = "name" @@ -144,6 +375,10 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { weight = 123 ignoreAutoTag = true favorite = true + endpoint1 = "endpoint1" + endpoint2 = "endpoint2" + stashID1 = "stashid1" + stashID2 = "stashid2" createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) @@ -186,8 +421,25 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { HairColor: models.NewOptionalString(hairColor), Weight: models.NewOptionalInt(weight), IgnoreAutoTag: models.NewOptionalBool(ignoreAutoTag), - CreatedAt: models.NewOptionalTime(createdAt), - UpdatedAt: models.NewOptionalTime(updatedAt), + TagIDs: &models.UpdateIDs{ + IDs: []int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}, + Mode: models.RelationshipUpdateModeSet, + }, + StashIDs: &models.UpdateStashIDs{ + StashIDs: []models.StashID{ + { + StashID: stashID1, + Endpoint: endpoint1, + }, + { + StashID: stashID2, + Endpoint: endpoint2, + }, + }, + Mode: models.RelationshipUpdateModeSet, + }, + CreatedAt: models.NewOptionalTime(createdAt), + UpdatedAt: models.NewOptionalTime(updatedAt), }, models.Performer{ ID: performerIDs[performerIdxWithDupName], @@ -215,11 +467,43 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { HairColor: hairColor, Weight: &weight, IgnoreAutoTag: ignoreAutoTag, - CreatedAt: createdAt, - UpdatedAt: updatedAt, + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{ + { + StashID: stashID1, + Endpoint: endpoint1, + }, + { + StashID: stashID2, + Endpoint: endpoint2, + }, + }), + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, + false, + }, + { + "clear all", + performerIDs[performerIdxWithTwoTags], + clearPerformerPartial(), + models.Performer{ + ID: performerIDs[performerIdxWithTwoTags], + Name: getPerformerStringValue(performerIdxWithTwoTags, "Name"), + Checksum: getPerformerStringValue(performerIdxWithTwoTags, checksumField), + Favorite: true, + TagIDs: models.NewRelatedIDs([]int{}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{}), }, false, }, + { + "invalid id", + invalidID, + models.PerformerPartial{}, + models.Performer{}, + true, + }, } for _, tt := range tests { qb := db.Performer @@ -237,6 +521,11 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { return } + if err := loadPerformerRelationships(ctx, tt.want, got); err != nil { + t.Errorf("loadPerformerRelationships() error = %v", err) + return + } + assert.Equal(tt.want, *got) s, err := qb.Find(ctx, tt.id) @@ -244,6 +533,12 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { t.Errorf("PerformerStore.Find() error = %v", err) } + // load relationships + if err := loadPerformerRelationships(ctx, tt.want, s); err != nil { + t.Errorf("loadPerformerRelationships() error = %v", err) + return + } + assert.Equal(tt.want, *s) }) } @@ -1156,23 +1451,78 @@ func TestPerformerStashIDs(t *testing.T) { if err := withRollbackTxn(func(ctx context.Context) error { qb := db.Performer - // create performer to test against - const name = "TestStashIDs" - performer := models.Performer{ - Name: name, - Checksum: md5.FromString(name), + // create scene to test against + const name = "TestPerformerStashIDs" + performer := &models.Performer{ + Name: name, } - err := qb.Create(ctx, &performer) - if err != nil { + if err := qb.Create(ctx, performer); err != nil { return fmt.Errorf("Error creating performer: %s", err.Error()) } - testStashIDReaderWriter(ctx, t, qb, performer.ID) + if err := performer.LoadStashIDs(ctx, qb); err != nil { + return err + } + + testPerformerStashIDs(ctx, t, performer) return nil }); err != nil { t.Error(err.Error()) } } + +func testPerformerStashIDs(ctx context.Context, t *testing.T, s *models.Performer) { + // ensure no stash IDs to begin with + assert.Len(t, s.StashIDs.List(), 0) + + // add stash ids + const stashIDStr = "stashID" + const endpoint = "endpoint" + stashID := models.StashID{ + StashID: stashIDStr, + Endpoint: endpoint, + } + + qb := db.Performer + + // update stash ids and ensure was updated + var err error + s, err = qb.UpdatePartial(ctx, s.ID, models.PerformerPartial{ + StashIDs: &models.UpdateStashIDs{ + StashIDs: []models.StashID{stashID}, + Mode: models.RelationshipUpdateModeSet, + }, + }) + if err != nil { + t.Error(err.Error()) + } + + if err := s.LoadStashIDs(ctx, qb); err != nil { + t.Error(err.Error()) + return + } + + assert.Equal(t, []models.StashID{stashID}, s.StashIDs.List()) + + // remove stash ids and ensure was updated + s, err = qb.UpdatePartial(ctx, s.ID, models.PerformerPartial{ + StashIDs: &models.UpdateStashIDs{ + StashIDs: []models.StashID{stashID}, + Mode: models.RelationshipUpdateModeRemove, + }, + }) + if err != nil { + t.Error(err.Error()) + } + + if err := s.LoadStashIDs(ctx, qb); err != nil { + t.Error(err.Error()) + return + } + + assert.Len(t, s.StashIDs.List(), 0) +} + func TestPerformerQueryRating(t *testing.T) { const rating = 3 ratingCriterion := models.IntCriterionInput{ diff --git a/pkg/sqlite/record.go b/pkg/sqlite/record.go index a0fb789efc7..5917a11c5e1 100644 --- a/pkg/sqlite/record.go +++ b/pkg/sqlite/record.go @@ -32,7 +32,7 @@ func (r *updateRecord) setNullString(destField string, v models.OptionalString) func (r *updateRecord) setBool(destField string, v models.OptionalBool) { if v.Set { if v.Null { - panic("null value not allowed in optional int") + panic("null value not allowed in optional bool") } r.set(destField, v.Value) } diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index af0c0f0a06f..7544b538c28 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -17,7 +17,6 @@ import ( "github.com/stashapp/stash/pkg/file" "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil/intslice" "github.com/stashapp/stash/pkg/sqlite" "github.com/stashapp/stash/pkg/txn" @@ -442,10 +441,9 @@ var ( ) var ( - performerTagLinks = [][2]int{ - {performerIdxWithTag, tagIdxWithPerformer}, - {performerIdxWithTwoTags, tagIdx1WithPerformer}, - {performerIdxWithTwoTags, tagIdx2WithPerformer}, + performerTags = linkMap{ + performerIdxWithTag: {tagIdxWithPerformer}, + performerIdxWithTwoTags: {tagIdx1WithPerformer, tagIdx2WithPerformer}, } ) @@ -552,14 +550,14 @@ func populateDB() error { return fmt.Errorf("error creating movies: %s", err.Error()) } - if err := createPerformers(ctx, performersNameCase, performersNameNoCase); err != nil { - return fmt.Errorf("error creating performers: %s", err.Error()) - } - if err := createTags(ctx, sqlite.TagReaderWriter, tagsNameCase, tagsNameNoCase); err != nil { return fmt.Errorf("error creating tags: %s", err.Error()) } + if err := createPerformers(ctx, performersNameCase, performersNameNoCase); err != nil { + return fmt.Errorf("error creating performers: %s", err.Error()) + } + if err := createStudios(ctx, sqlite.StudioReaderWriter, studiosNameCase, studiosNameNoCase); err != nil { return fmt.Errorf("error creating studios: %s", err.Error()) } @@ -584,10 +582,6 @@ func populateDB() error { return fmt.Errorf("error creating saved filters: %s", err.Error()) } - if err := linkPerformerTags(ctx); err != nil { - return fmt.Errorf("error linking performer tags: %s", err.Error()) - } - if err := linkMovieStudios(ctx, sqlite.MovieReaderWriter); err != nil { return fmt.Errorf("error linking movie studios: %s", err.Error()) } @@ -1300,6 +1294,8 @@ func createPerformers(ctx context.Context, n int, o int) error { } // so count backwards to 0 as needed // performers [ i ] and [ n + o - i - 1 ] should have similar names with only the Name!=NaMe part different + tids := indexesToIDs(tagIDs, performerTags[i]) + performer := models.Performer{ Name: getPerformerStringValue(index, name), Checksum: getPerformerStringValue(i, checksumField), @@ -1311,6 +1307,7 @@ func createPerformers(ctx context.Context, n int, o int) error { Ethnicity: getPerformerStringValue(i, "Ethnicity"), Rating: getIntPtr(getRating(i)), IgnoreAutoTag: getIgnoreAutoTag(i), + TagIDs: models.NewRelatedIDs(tids), } careerLength := getPerformerCareerLength(i) @@ -1318,20 +1315,18 @@ func createPerformers(ctx context.Context, n int, o int) error { performer.CareerLength = *careerLength } + if (index+1)%5 != 0 { + performer.StashIDs = models.NewRelatedStashIDs([]models.StashID{ + performerStashID(i), + }) + } + err := pqb.Create(ctx, &performer) if err != nil { return fmt.Errorf("Error creating performer %v+: %s", performer, err.Error()) } - if (index+1)%5 != 0 { - if err := pqb.UpdateStashIDs(ctx, performer.ID, []models.StashID{ - performerStashID(i), - }); err != nil { - return fmt.Errorf("setting performer stash ids: %w", err) - } - } - performerIDs = append(performerIDs, performer.ID) performerNames = append(performerNames, performer.Name) } @@ -1602,22 +1597,6 @@ func doLinks(links [][2]int, fn func(idx1, idx2 int) error) error { return nil } -func linkPerformerTags(ctx context.Context) error { - qb := db.Performer - return doLinks(performerTagLinks, func(performerIndex, tagIndex int) error { - performerID := performerIDs[performerIndex] - tagID := tagIDs[tagIndex] - tagIDs, err := qb.GetTagIDs(ctx, performerID) - if err != nil { - return err - } - - tagIDs = intslice.IntAppendUnique(tagIDs, tagID) - - return qb.UpdateTags(ctx, performerID, tagIDs) - }) -} - func linkMovieStudios(ctx context.Context, mqb models.MovieWriter) error { return doLinks(movieStudioLinks, func(movieIndex, studioIndex int) error { movie := models.MoviePartial{ diff --git a/pkg/sqlite/tables.go b/pkg/sqlite/tables.go index 99301d0469a..671a758feba 100644 --- a/pkg/sqlite/tables.go +++ b/pkg/sqlite/tables.go @@ -183,6 +183,21 @@ var ( table: goqu.T(performerTable), idColumn: goqu.T(performerTable).Col(idColumn), } + + performersTagsTableMgr = &joinTable{ + table: table{ + table: performersTagsJoinTable, + idColumn: performersTagsJoinTable.Col(performerIDColumn), + }, + fkColumn: performersTagsJoinTable.Col(tagIDColumn), + } + + performersStashIDsTableMgr = &stashIDTable{ + table: table{ + table: performersStashIDsJoinTable, + idColumn: performersStashIDsJoinTable.Col(performerIDColumn), + }, + } ) var ( From 929cc4a59c31810af72122c38323242787ba0b25 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 2 Nov 2022 23:41:30 +0000 Subject: [PATCH 02/24] Remove checksum from performer --- graphql/schema/types/performer.graphql | 4 +-- internal/api/resolver_model_performer.go | 5 ++++ internal/api/resolver_mutation_performer.go | 14 +--------- internal/autotag/integration_test.go | 3 +- internal/identify/performer.go | 2 -- internal/identify/performer_test.go | 5 +--- internal/manager/task_export.go | 6 ++-- internal/manager/task_stash_box_tag.go | 4 --- pkg/models/model_performer.go | 5 ---- pkg/performer/export_test.go | 2 -- pkg/performer/import.go | 4 --- pkg/performer/import_test.go | 2 -- .../39_performer_disambig_aliases.up.sql | 2 ++ pkg/sqlite/performer.go | 28 +++++-------------- pkg/sqlite/performer_test.go | 20 +++---------- pkg/sqlite/record.go | 16 +++++------ pkg/sqlite/setup_test.go | 1 - pkg/sqlite/table.go | 17 ++++++++--- 18 files changed, 47 insertions(+), 93 deletions(-) create mode 100644 pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql diff --git a/graphql/schema/types/performer.graphql b/graphql/schema/types/performer.graphql index 1a20026107f..72728676515 100644 --- a/graphql/schema/types/performer.graphql +++ b/graphql/schema/types/performer.graphql @@ -9,8 +9,8 @@ enum GenderEnum { type Performer { id: ID! - checksum: String! - name: String + checksum: String @deprecated(reason: "Not used") + name: String! url: String gender: GenderEnum twitter: String diff --git a/internal/api/resolver_model_performer.go b/internal/api/resolver_model_performer.go index 807f5294f13..e9c90877a35 100644 --- a/internal/api/resolver_model_performer.go +++ b/internal/api/resolver_model_performer.go @@ -11,6 +11,11 @@ import ( "github.com/stashapp/stash/pkg/models" ) +// Checksum is deprecated +func (r *performerResolver) Checksum(ctx context.Context, obj *models.Performer) (*string, error) { + return nil, nil +} + func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) { if obj.Height != nil { ret := strconv.Itoa(*obj.Height) diff --git a/internal/api/resolver_mutation_performer.go b/internal/api/resolver_mutation_performer.go index 44d3293047a..b2a143bf3c4 100644 --- a/internal/api/resolver_mutation_performer.go +++ b/internal/api/resolver_mutation_performer.go @@ -6,7 +6,6 @@ import ( "strconv" "time" - "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/performer" "github.com/stashapp/stash/pkg/plugin" @@ -36,9 +35,6 @@ func stashIDPtrSliceToSlice(v []*models.StashID) []models.StashID { } func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerCreateInput) (*models.Performer, error) { - // generate checksum from performer name rather than image - checksum := md5.FromString(input.Name) - var imageData []byte var err error @@ -59,7 +55,6 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC currentTime := time.Now() newPerformer := models.Performer{ Name: input.Name, - Checksum: checksum, TagIDs: models.NewRelatedIDs(tagIDs), StashIDs: models.NewRelatedStashIDs(stashIDPtrSliceToSlice(input.StashIds)), CreatedAt: currentTime, @@ -191,14 +186,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU } } - if input.Name != nil { - // generate checksum from performer name rather than image - checksum := md5.FromString(*input.Name) - - updatedPerformer.Name = models.NewOptionalString(*input.Name) - updatedPerformer.Checksum = models.NewOptionalString(checksum) - } - + updatedPerformer.Name = translator.optionalString(input.Name, "name") updatedPerformer.URL = translator.optionalString(input.URL, "url") if translator.hasField("gender") { diff --git a/internal/autotag/integration_test.go b/internal/autotag/integration_test.go index ddf9adc95be..6ffd12263a9 100644 --- a/internal/autotag/integration_test.go +++ b/internal/autotag/integration_test.go @@ -86,8 +86,7 @@ func TestMain(m *testing.M) { func createPerformer(ctx context.Context, pqb models.PerformerWriter) error { // create the performer performer := models.Performer{ - Checksum: testName, - Name: testName, + Name: testName, } err := pqb.Create(ctx, &performer) diff --git a/internal/identify/performer.go b/internal/identify/performer.go index 5108905a35d..d65ff995e61 100644 --- a/internal/identify/performer.go +++ b/internal/identify/performer.go @@ -6,7 +6,6 @@ import ( "strconv" "time" - "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/models" ) @@ -53,7 +52,6 @@ func scrapedToPerformerInput(performer *models.ScrapedPerformer) models.Performe currentTime := time.Now() ret := models.Performer{ Name: *performer.Name, - Checksum: md5.FromString(*performer.Name), CreatedAt: currentTime, UpdatedAt: currentTime, } diff --git a/internal/identify/performer_test.go b/internal/identify/performer_test.go index 7e71bdcdb32..94fa24287a3 100644 --- a/internal/identify/performer_test.go +++ b/internal/identify/performer_test.go @@ -205,7 +205,6 @@ func Test_createMissingPerformer(t *testing.T) { func Test_scrapedToPerformerInput(t *testing.T) { name := "name" - md5 := "b068931cc450442b63f5b3d276ea4297" var stringValues []string for i := 0; i < 17; i++ { @@ -258,7 +257,6 @@ func Test_scrapedToPerformerInput(t *testing.T) { }, models.Performer{ Name: name, - Checksum: md5, Birthdate: dateToDatePtr(models.NewDate(*nextVal())), DeathDate: dateToDatePtr(models.NewDate(*nextVal())), Gender: models.GenderEnum(*nextVal()), @@ -284,8 +282,7 @@ func Test_scrapedToPerformerInput(t *testing.T) { Name: &name, }, models.Performer{ - Name: name, - Checksum: md5, + Name: name, }, }, } diff --git a/internal/manager/task_export.go b/internal/manager/task_export.go index b968a5e76c4..a3cf9215294 100644 --- a/internal/manager/task_export.go +++ b/internal/manager/task_export.go @@ -895,13 +895,13 @@ func (t *ExportTask) exportPerformer(ctx context.Context, wg *sync.WaitGroup, jo newPerformerJSON, err := performer.ToJSON(ctx, performerReader, p) if err != nil { - logger.Errorf("[performers] <%s> error getting performer JSON: %s", p.Checksum, err.Error()) + logger.Errorf("[performers] <%s> error getting performer JSON: %s", p.Name, err.Error()) continue } tags, err := repo.Tag.FindByPerformerID(ctx, p.ID) if err != nil { - logger.Errorf("[performers] <%s> error getting performer tags: %s", p.Checksum, err.Error()) + logger.Errorf("[performers] <%s> error getting performer tags: %s", p.Name, err.Error()) continue } @@ -914,7 +914,7 @@ func (t *ExportTask) exportPerformer(ctx context.Context, wg *sync.WaitGroup, jo fn := newPerformerJSON.Filename() if err := t.json.savePerformer(fn, newPerformerJSON); err != nil { - logger.Errorf("[performers] <%s> failed to save json: %s", p.Checksum, err.Error()) + logger.Errorf("[performers] <%s> failed to save json: %s", p.Name, err.Error()) } } } diff --git a/internal/manager/task_stash_box_tag.go b/internal/manager/task_stash_box_tag.go index ba7f0e8d37f..4ca1be256fc 100644 --- a/internal/manager/task_stash_box_tag.go +++ b/internal/manager/task_stash_box_tag.go @@ -6,7 +6,6 @@ import ( "strconv" "time" - "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/scraper/stashbox" @@ -134,8 +133,6 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { } if excluded["name"] && performer.Name != nil { partial.Name = models.NewOptionalString(*performer.Name) - checksum := md5.FromString(*performer.Name) - partial.Checksum = models.NewOptionalString(checksum) } if performer.Piercings != nil && !excluded["piercings"] { partial.Piercings = models.NewOptionalString(*performer.Piercings) @@ -194,7 +191,6 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { Aliases: getString(performer.Aliases), Birthdate: getDate(performer.Birthdate), CareerLength: getString(performer.CareerLength), - Checksum: md5.FromString(*performer.Name), Country: getString(performer.Country), CreatedAt: currentTime, Ethnicity: getString(performer.Ethnicity), diff --git a/pkg/models/model_performer.go b/pkg/models/model_performer.go index d11481b99af..12327ac261e 100644 --- a/pkg/models/model_performer.go +++ b/pkg/models/model_performer.go @@ -3,13 +3,10 @@ package models import ( "context" "time" - - "github.com/stashapp/stash/pkg/hash/md5" ) type Performer struct { ID int `json:"id"` - Checksum string `json:"checksum"` Name string `json:"name"` Gender GenderEnum `json:"gender"` URL string `json:"url"` @@ -68,7 +65,6 @@ func (s *Performer) LoadRelationships(ctx context.Context, l PerformerReader) er // the database entry. type PerformerPartial struct { ID int - Checksum OptionalString Name OptionalString Gender OptionalString URL OptionalString @@ -102,7 +98,6 @@ type PerformerPartial struct { func NewPerformer(name string) *Performer { currentTime := time.Now() return &Performer{ - Checksum: md5.FromString(name), Name: name, CreatedAt: currentTime, UpdatedAt: currentTime, diff --git a/pkg/performer/export_test.go b/pkg/performer/export_test.go index e41b5509fff..312db1930bd 100644 --- a/pkg/performer/export_test.go +++ b/pkg/performer/export_test.go @@ -4,7 +4,6 @@ import ( "errors" "strconv" - "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/json" "github.com/stashapp/stash/pkg/models/jsonschema" @@ -72,7 +71,6 @@ func createFullPerformer(id int, name string) *models.Performer { return &models.Performer{ ID: id, Name: name, - Checksum: md5.FromString(name), URL: url, Aliases: aliases, Birthdate: &birthDate, diff --git a/pkg/performer/import.go b/pkg/performer/import.go index 76e4ab09fda..10bfd53836b 100644 --- a/pkg/performer/import.go +++ b/pkg/performer/import.go @@ -6,7 +6,6 @@ import ( "strconv" "strings" - "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/jsonschema" @@ -168,11 +167,8 @@ func (i *Importer) Update(ctx context.Context, id int) error { } func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Performer { - checksum := md5.FromString(performerJSON.Name) - newPerformer := models.Performer{ Name: performerJSON.Name, - Checksum: checksum, Gender: models.GenderEnum(performerJSON.Gender), URL: performerJSON.URL, Ethnicity: performerJSON.Ethnicity, diff --git a/pkg/performer/import_test.go b/pkg/performer/import_test.go index 41da960003c..cc2170f40aa 100644 --- a/pkg/performer/import_test.go +++ b/pkg/performer/import_test.go @@ -6,7 +6,6 @@ import ( "github.com/stretchr/testify/mock" - "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/jsonschema" "github.com/stashapp/stash/pkg/models/mocks" @@ -60,7 +59,6 @@ func TestImporterPreImport(t *testing.T) { assert.Nil(t, err) expectedPerformer := *createFullPerformer(0, performerName) - expectedPerformer.Checksum = md5.FromString(performerName) assert.Equal(t, expectedPerformer, i.performer) } diff --git a/pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql b/pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql new file mode 100644 index 00000000000..16ed777b673 --- /dev/null +++ b/pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql @@ -0,0 +1,2 @@ +DROP INDEX `performers_checksum_unique`; +ALTER TABLE `performers` DROP COLUMN `checksum`; diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index 38a68a3ec68..c13ac4beadf 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -24,8 +24,7 @@ const performersImageTable = "performers_image" // performer cover image type performerRow struct { ID int `db:"id" goqu:"skipinsert"` - Checksum string `db:"checksum"` - Name zero.String `db:"name"` + Name string `db:"name"` Gender zero.String `db:"gender"` URL zero.String `db:"url"` Twitter zero.String `db:"twitter"` @@ -54,8 +53,7 @@ type performerRow struct { func (r *performerRow) fromPerformer(o models.Performer) { r.ID = o.ID - r.Checksum = o.Checksum - r.Name = zero.StringFrom(o.Name) + r.Name = o.Name if o.Gender.IsValid() { r.Gender = zero.StringFrom(o.Gender.String()) } @@ -91,8 +89,7 @@ func (r *performerRow) fromPerformer(o models.Performer) { func (r *performerRow) resolve() *models.Performer { ret := &models.Performer{ ID: r.ID, - Checksum: r.Checksum, - Name: r.Name.String, + Name: r.Name, Gender: models.GenderEnum(r.Gender.String), URL: r.URL.String, Twitter: r.Twitter.String, @@ -127,8 +124,7 @@ type performerRowRecord struct { } func (r *performerRowRecord) fromPartial(o models.PerformerPartial) { - r.setNullString("checksum", o.Checksum) - r.setNullString("name", o.Name) + r.setString("name", o.Name) r.setNullString("gender", o.Gender) r.setNullString("url", o.URL) r.setNullString("twitter", o.Twitter) @@ -560,7 +556,7 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform query.handleCriterion(ctx, intCriterionHandler(filter.Weight, tableName+".weight", nil)) query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { if filter.StashID != nil { - qb.stashIDRepository().join(f, "performer_stash_ids", "performers.id") + performersStashIDsTableMgr.join(f, "performer_stash_ids", "performers.id") stringCriterionHandler(filter.StashID, "performer_stash_ids.stash_id")(ctx, f) } })) @@ -628,7 +624,7 @@ func performerIsMissingCriterionHandler(qb *PerformerStore, isMissing *string) c f.addLeftJoin(performersImageTable, "image_join", "image_join.performer_id = performers.id") f.addWhere("image_join.performer_id IS NULL") case "stash_id": - qb.stashIDRepository().join(f, "performer_stash_ids", "performers.id") + performersStashIDsTableMgr.join(f, "performer_stash_ids", "performers.id") f.addWhere("performer_stash_ids.performer_id IS NULL") default: f.addWhere("(performers." + *isMissing + " IS NULL OR TRIM(performers." + *isMissing + ") = '')") @@ -857,18 +853,8 @@ func (qb *PerformerStore) DestroyImage(ctx context.Context, performerID int) err return qb.imageRepository().destroy(ctx, []int{performerID}) } -func (qb *PerformerStore) stashIDRepository() *stashIDRepository { - return &stashIDRepository{ - repository{ - tx: qb.tx, - tableName: "performer_stash_ids", - idColumn: performerIDColumn, - }, - } -} - func (qb *PerformerStore) GetStashIDs(ctx context.Context, performerID int) ([]models.StashID, error) { - return qb.stashIDRepository().get(ctx, performerID) + return performersStashIDsTableMgr.get(ctx, performerID) } func (qb *PerformerStore) FindByStashID(ctx context.Context, stashID models.StashID) ([]*models.Performer, error) { diff --git a/pkg/sqlite/performer_test.go b/pkg/sqlite/performer_test.go index 99ecd8e641b..e85725f445a 100644 --- a/pkg/sqlite/performer_test.go +++ b/pkg/sqlite/performer_test.go @@ -12,7 +12,6 @@ import ( "testing" "time" - "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/models" "github.com/stretchr/testify/assert" ) @@ -36,7 +35,6 @@ func Test_PerformerStore_Create(t *testing.T) { var ( name = "name" gender = models.GenderEnumFemale - checksum = "checksum" details = "details" url = "url" twitter = "twitter" @@ -76,7 +74,6 @@ func Test_PerformerStore_Create(t *testing.T) { "full", models.Performer{ Name: name, - Checksum: checksum, Gender: gender, URL: url, Twitter: twitter, @@ -118,9 +115,8 @@ func Test_PerformerStore_Create(t *testing.T) { { "invalid tag id", models.Performer{ - Name: name, - Checksum: checksum, - TagIDs: models.NewRelatedIDs([]int{invalidID}), + Name: name, + TagIDs: models.NewRelatedIDs([]int{invalidID}), }, true, }, @@ -181,7 +177,6 @@ func Test_PerformerStore_Update(t *testing.T) { var ( name = "name" gender = models.GenderEnumFemale - checksum = "checksum" details = "details" url = "url" twitter = "twitter" @@ -222,7 +217,6 @@ func Test_PerformerStore_Update(t *testing.T) { &models.Performer{ ID: performerIDs[performerIdxWithGallery], Name: name, - Checksum: checksum, Gender: gender, URL: url, Twitter: twitter, @@ -355,7 +349,6 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { var ( name = "name" gender = models.GenderEnumFemale - checksum = "checksum" details = "details" url = "url" twitter = "twitter" @@ -398,7 +391,6 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { performerIDs[performerIdxWithDupName], models.PerformerPartial{ Name: models.NewOptionalString(name), - Checksum: models.NewOptionalString(checksum), Gender: models.NewOptionalString(gender.String()), URL: models.NewOptionalString(url), Twitter: models.NewOptionalString(twitter), @@ -444,7 +436,6 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { models.Performer{ ID: performerIDs[performerIdxWithDupName], Name: name, - Checksum: checksum, Gender: gender, URL: url, Twitter: twitter, @@ -490,7 +481,6 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { models.Performer{ ID: performerIDs[performerIdxWithTwoTags], Name: getPerformerStringValue(performerIdxWithTwoTags, "Name"), - Checksum: getPerformerStringValue(performerIdxWithTwoTags, checksumField), Favorite: true, TagIDs: models.NewRelatedIDs([]int{}), StashIDs: models.NewRelatedStashIDs([]models.StashID{}), @@ -904,8 +894,7 @@ func TestPerformerUpdatePerformerImage(t *testing.T) { // create performer to test against const name = "TestPerformerUpdatePerformerImage" performer := models.Performer{ - Name: name, - Checksum: md5.FromString(name), + Name: name, } err := qb.Create(ctx, &performer) if err != nil { @@ -944,8 +933,7 @@ func TestPerformerDestroyPerformerImage(t *testing.T) { // create performer to test against const name = "TestPerformerDestroyPerformerImage" performer := models.Performer{ - Name: name, - Checksum: md5.FromString(name), + Name: name, } err := qb.Create(ctx, &performer) if err != nil { diff --git a/pkg/sqlite/record.go b/pkg/sqlite/record.go index 5917a11c5e1..65abfe5211a 100644 --- a/pkg/sqlite/record.go +++ b/pkg/sqlite/record.go @@ -14,14 +14,14 @@ func (r *updateRecord) set(destField string, v interface{}) { r.Record[destField] = v } -// func (r *updateRecord) setString(destField string, v models.OptionalString) { -// if v.Set { -// if v.Null { -// panic("null value not allowed in optional string") -// } -// r.set(destField, v.Value) -// } -// } +func (r *updateRecord) setString(destField string, v models.OptionalString) { + if v.Set { + if v.Null { + panic("null value not allowed in optional string") + } + r.set(destField, v.Value) + } +} func (r *updateRecord) setNullString(destField string, v models.OptionalString) { if v.Set { diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index 7544b538c28..0c384ecf3ec 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -1298,7 +1298,6 @@ func createPerformers(ctx context.Context, n int, o int) error { performer := models.Performer{ Name: getPerformerStringValue(index, name), - Checksum: getPerformerStringValue(i, checksumField), URL: getPerformerNullStringValue(i, urlField), Favorite: getPerformerBoolValue(i), Birthdate: getPerformerBirthdate(i), diff --git a/pkg/sqlite/table.go b/pkg/sqlite/table.go index fbb3bbb89bb..11f21d7b4d3 100644 --- a/pkg/sqlite/table.go +++ b/pkg/sqlite/table.go @@ -129,6 +129,15 @@ func (t *table) destroy(ctx context.Context, ids []int) error { return nil } +func (t *table) join(j joiner, as string, parentIDCol string) { + tableName := t.table.GetTable() + tt := tableName + if as != "" { + tt = as + } + j.addLeftJoin(tableName, as, fmt.Sprintf("%s.%s = %s", tt, t.idColumn.GetCol(), parentIDCol)) +} + // func (t *table) get(ctx context.Context, q *goqu.SelectDataset, dest interface{}) error { // tx, err := getTx(ctx) // if err != nil { @@ -258,18 +267,18 @@ type stashIDRow struct { Endpoint null.String `db:"endpoint"` } -func (r *stashIDRow) resolve() *models.StashID { - return &models.StashID{ +func (r *stashIDRow) resolve() models.StashID { + return models.StashID{ StashID: r.StashID.String, Endpoint: r.Endpoint.String, } } -func (t *stashIDTable) get(ctx context.Context, id int) ([]*models.StashID, error) { +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)) const single = false - var ret []*models.StashID + var ret []models.StashID if err := queryFunc(ctx, q, single, func(rows *sqlx.Rows) error { var v stashIDRow if err := rows.StructScan(&v); err != nil { From a063fee15420941e34610481fbc5f8a432ab3d60 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 2 Nov 2022 23:17:01 +0000 Subject: [PATCH 03/24] Add disambiguation, overhaul aliases --- graphql/schema/types/performer.graphql | 20 +- internal/api/resolver_model_performer.go | 26 ++ internal/api/resolver_mutation_performer.go | 32 +- internal/identify/performer.go | 3 +- internal/identify/performer_test.go | 2 +- internal/manager/task_stash_box_tag.go | 8 +- pkg/models/jsonschema/performer.go | 75 +++- pkg/models/jsonschema/performer_test.go | 52 +++ pkg/models/mocks/PerformerReaderWriter.go | 61 ++- pkg/models/model_performer.go | 116 ++--- pkg/models/performer.go | 1 + pkg/models/relationships.go | 62 +++ pkg/models/update.go | 5 + pkg/performer/export.go | 32 +- pkg/performer/export_test.go | 38 +- pkg/performer/import.go | 2 +- pkg/scraper/stashbox/stash_box.go | 15 +- .../stringslice/string_collections.go | 28 +- .../39_performer_disambig_aliases.up.sql | 14 + pkg/sqlite/migrations/39_postmigrate.go | 130 ++++++ pkg/sqlite/performer.go | 110 +++-- pkg/sqlite/performer_test.go | 405 +++++++++--------- pkg/sqlite/setup_test.go | 22 +- pkg/sqlite/table.go | 97 +++++ pkg/sqlite/tables.go | 9 + 25 files changed, 966 insertions(+), 399 deletions(-) create mode 100644 pkg/models/jsonschema/performer_test.go create mode 100644 pkg/sqlite/migrations/39_postmigrate.go diff --git a/graphql/schema/types/performer.graphql b/graphql/schema/types/performer.graphql index 72728676515..cfab68cb396 100644 --- a/graphql/schema/types/performer.graphql +++ b/graphql/schema/types/performer.graphql @@ -11,6 +11,7 @@ type Performer { id: ID! checksum: String @deprecated(reason: "Not used") name: String! + disambiguation: String url: String gender: GenderEnum twitter: String @@ -26,7 +27,8 @@ type Performer { career_length: String tattoos: String piercings: String - aliases: String + aliases: String @deprecated(reason: "Use alias_list") + alias_list: [String!] favorite: Boolean! tags: [Tag!]! ignore_auto_tag: Boolean! @@ -50,6 +52,7 @@ type Performer { input PerformerCreateInput { name: String! + disambiguation: String url: String gender: GenderEnum birthdate: String @@ -64,7 +67,8 @@ input PerformerCreateInput { career_length: String tattoos: String piercings: String - aliases: String + aliases: String @deprecated(reason: "Use alias_list") + alias_list: [String!] twitter: String instagram: String favorite: Boolean @@ -83,6 +87,7 @@ input PerformerCreateInput { input PerformerUpdateInput { id: ID! name: String + disambiguation: String url: String gender: GenderEnum birthdate: String @@ -97,7 +102,8 @@ input PerformerUpdateInput { career_length: String tattoos: String piercings: String - aliases: String + aliases: String @deprecated(reason: "Use alias_list") + alias_list: [String!] twitter: String instagram: String favorite: Boolean @@ -113,6 +119,11 @@ input PerformerUpdateInput { ignore_auto_tag: Boolean } +input BulkUpdateStrings { + values: [String!] + mode: BulkUpdateIdMode! +} + input BulkPerformerUpdateInput { clientMutationId: String ids: [ID!] @@ -130,7 +141,8 @@ input BulkPerformerUpdateInput { career_length: String tattoos: String piercings: String - aliases: String + aliases: String @deprecated(reason: "Use alias_list") + alias_list: BulkUpdateStrings twitter: String instagram: String favorite: Boolean diff --git a/internal/api/resolver_model_performer.go b/internal/api/resolver_model_performer.go index e9c90877a35..ee6aef549a2 100644 --- a/internal/api/resolver_model_performer.go +++ b/internal/api/resolver_model_performer.go @@ -3,6 +3,7 @@ package api import ( "context" "strconv" + "strings" "github.com/stashapp/stash/internal/api/loaders" "github.com/stashapp/stash/internal/api/urlbuilders" @@ -16,6 +17,31 @@ func (r *performerResolver) Checksum(ctx context.Context, obj *models.Performer) return nil, nil } +func (r *performerResolver) Aliases(ctx context.Context, obj *models.Performer) (*string, error) { + if !obj.Aliases.Loaded() { + if err := r.withTxn(ctx, func(ctx context.Context) error { + return obj.LoadAliases(ctx, r.repository.Performer) + }); err != nil { + return nil, err + } + } + + ret := strings.Join(obj.Aliases.List(), ", ") + return &ret, nil +} + +func (r *performerResolver) AliasList(ctx context.Context, obj *models.Performer) ([]string, error) { + if !obj.Aliases.Loaded() { + if err := r.withTxn(ctx, func(ctx context.Context) error { + return obj.LoadAliases(ctx, r.repository.Performer) + }); err != nil { + return nil, err + } + } + + return obj.Aliases.List(), nil +} + func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) { if obj.Height != nil { ret := strconv.Itoa(*obj.Height) diff --git a/internal/api/resolver_mutation_performer.go b/internal/api/resolver_mutation_performer.go index b2a143bf3c4..02a2e77fcdb 100644 --- a/internal/api/resolver_mutation_performer.go +++ b/internal/api/resolver_mutation_performer.go @@ -104,8 +104,10 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC if input.Piercings != nil { newPerformer.Piercings = *input.Piercings } - if input.Aliases != nil { - newPerformer.Aliases = *input.Aliases + if input.AliasList != nil { + newPerformer.Aliases = models.NewRelatedStrings(input.AliasList) + } else if input.Aliases != nil { + newPerformer.Aliases = models.NewRelatedStrings(stringslice.FromString(*input.Aliases, ",")) } if input.Twitter != nil { newPerformer.Twitter = *input.Twitter @@ -216,7 +218,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length") updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos") updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings") - updatedPerformer.Aliases = translator.optionalString(input.Aliases, "aliases") updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter") updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram") updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite") @@ -227,6 +228,18 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight") updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") + if translator.hasField("alias_list") { + updatedPerformer.Aliases = &models.UpdateStrings{ + Values: input.AliasList, + Mode: models.RelationshipUpdateModeSet, + } + } else if translator.hasField("aliases") { + updatedPerformer.Aliases = &models.UpdateStrings{ + Values: stringslice.FromString(*input.Aliases, ","), + Mode: models.RelationshipUpdateModeSet, + } + } + if translator.hasField("tag_ids") { updatedPerformer.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet) if err != nil { @@ -321,7 +334,6 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length") updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos") updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings") - updatedPerformer.Aliases = translator.optionalString(input.Aliases, "aliases") updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter") updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram") updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite") @@ -332,6 +344,18 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight") updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") + if translator.hasField("alias_list") { + updatedPerformer.Aliases = &models.UpdateStrings{ + Values: input.AliasList.Values, + Mode: input.AliasList.Mode, + } + } else if translator.hasField("aliases") { + updatedPerformer.Aliases = &models.UpdateStrings{ + Values: stringslice.FromString(*input.Aliases, ","), + Mode: models.RelationshipUpdateModeSet, + } + } + if translator.hasField("gender") { if input.Gender != nil { updatedPerformer.Gender = models.NewOptionalString(input.Gender.String()) diff --git a/internal/identify/performer.go b/internal/identify/performer.go index d65ff995e61..a78a0ce6c79 100644 --- a/internal/identify/performer.go +++ b/internal/identify/performer.go @@ -7,6 +7,7 @@ import ( "time" "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) type PerformerCreator interface { @@ -106,7 +107,7 @@ func scrapedToPerformerInput(performer *models.ScrapedPerformer) models.Performe ret.Piercings = *performer.Piercings } if performer.Aliases != nil { - ret.Aliases = *performer.Aliases + ret.Aliases = models.NewRelatedStrings(stringslice.FromString(*performer.Aliases, ",")) } if performer.Twitter != nil { ret.Twitter = *performer.Twitter diff --git a/internal/identify/performer_test.go b/internal/identify/performer_test.go index 94fa24287a3..0a78ea17358 100644 --- a/internal/identify/performer_test.go +++ b/internal/identify/performer_test.go @@ -271,7 +271,7 @@ func Test_scrapedToPerformerInput(t *testing.T) { CareerLength: *nextVal(), Tattoos: *nextVal(), Piercings: *nextVal(), - Aliases: *nextVal(), + Aliases: models.NewRelatedStrings([]string{*nextVal()}), Twitter: *nextVal(), Instagram: *nextVal(), }, diff --git a/internal/manager/task_stash_box_tag.go b/internal/manager/task_stash_box_tag.go index 4ca1be256fc..a60a4f97193 100644 --- a/internal/manager/task_stash_box_tag.go +++ b/internal/manager/task_stash_box_tag.go @@ -9,6 +9,7 @@ import ( "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/scraper/stashbox" + "github.com/stashapp/stash/pkg/sliceutil/stringslice" "github.com/stashapp/stash/pkg/txn" "github.com/stashapp/stash/pkg/utils" ) @@ -89,7 +90,10 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { partial := models.NewPerformerPartial() if performer.Aliases != nil && !excluded["aliases"] { - partial.Aliases = models.NewOptionalString(*performer.Aliases) + partial.Aliases = &models.UpdateStrings{ + Values: stringslice.FromString(*performer.Aliases, ","), + Mode: models.RelationshipUpdateModeSet, + } } if performer.Birthdate != nil && *performer.Birthdate != "" && !excluded["birthdate"] { value := getDate(performer.Birthdate) @@ -188,7 +192,7 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { } else if t.name != nil && performer.Name != nil { currentTime := time.Now() newPerformer := models.Performer{ - Aliases: getString(performer.Aliases), + Aliases: models.NewRelatedStrings(stringslice.FromString(*performer.Aliases, ",")), Birthdate: getDate(performer.Birthdate), CareerLength: getString(performer.CareerLength), Country: getString(performer.Country), diff --git a/pkg/models/jsonschema/performer.go b/pkg/models/jsonschema/performer.go index e4f5de2cb27..0a98ff9a9fc 100644 --- a/pkg/models/jsonschema/performer.go +++ b/pkg/models/jsonschema/performer.go @@ -2,14 +2,37 @@ package jsonschema import ( "fmt" + "io" "os" jsoniter "github.com/json-iterator/go" "github.com/stashapp/stash/pkg/fsutil" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/json" + "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) +type StringOrStringList []string + +func (s *StringOrStringList) UnmarshalJSON(data []byte) error { + var stringList []string + var stringVal string + + err := jsoniter.Unmarshal(data, &stringList) + if err == nil { + *s = stringList + return nil + } + + err = jsoniter.Unmarshal(data, &stringVal) + if err == nil { + *s = stringslice.FromString(stringVal, ",") + return nil + } + + return err +} + type Performer struct { Name string `json:"name,omitempty"` Gender string `json:"gender,omitempty"` @@ -21,25 +44,25 @@ type Performer struct { Country string `json:"country,omitempty"` EyeColor string `json:"eye_color,omitempty"` // this should be int, but keeping string for backwards compatibility - Height string `json:"height,omitempty"` - Measurements string `json:"measurements,omitempty"` - FakeTits string `json:"fake_tits,omitempty"` - CareerLength string `json:"career_length,omitempty"` - Tattoos string `json:"tattoos,omitempty"` - Piercings string `json:"piercings,omitempty"` - Aliases string `json:"aliases,omitempty"` - Favorite bool `json:"favorite,omitempty"` - Tags []string `json:"tags,omitempty"` - Image string `json:"image,omitempty"` - CreatedAt json.JSONTime `json:"created_at,omitempty"` - UpdatedAt json.JSONTime `json:"updated_at,omitempty"` - Rating int `json:"rating,omitempty"` - Details string `json:"details,omitempty"` - DeathDate string `json:"death_date,omitempty"` - HairColor string `json:"hair_color,omitempty"` - Weight int `json:"weight,omitempty"` - StashIDs []models.StashID `json:"stash_ids,omitempty"` - IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"` + Height string `json:"height,omitempty"` + Measurements string `json:"measurements,omitempty"` + FakeTits string `json:"fake_tits,omitempty"` + CareerLength string `json:"career_length,omitempty"` + Tattoos string `json:"tattoos,omitempty"` + Piercings string `json:"piercings,omitempty"` + Aliases StringOrStringList `json:"aliases,omitempty"` + Favorite bool `json:"favorite,omitempty"` + Tags []string `json:"tags,omitempty"` + Image string `json:"image,omitempty"` + CreatedAt json.JSONTime `json:"created_at,omitempty"` + UpdatedAt json.JSONTime `json:"updated_at,omitempty"` + Rating int `json:"rating,omitempty"` + Details string `json:"details,omitempty"` + DeathDate string `json:"death_date,omitempty"` + HairColor string `json:"hair_color,omitempty"` + Weight int `json:"weight,omitempty"` + StashIDs []models.StashID `json:"stash_ids,omitempty"` + IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"` } func (s Performer) Filename() string { @@ -47,18 +70,24 @@ func (s Performer) Filename() string { } func LoadPerformerFile(filePath string) (*Performer, error) { - var performer Performer file, err := os.Open(filePath) if err != nil { return nil, err } defer file.Close() + + return loadPerformer(file) +} + +func loadPerformer(r io.ReadSeeker) (*Performer, error) { var json = jsoniter.ConfigCompatibleWithStandardLibrary - jsonParser := json.NewDecoder(file) - err = jsonParser.Decode(&performer) - if err != nil { + jsonParser := json.NewDecoder(r) + + var performer Performer + if err := jsonParser.Decode(&performer); err != nil { return nil, err } + return &performer, nil } diff --git a/pkg/models/jsonschema/performer_test.go b/pkg/models/jsonschema/performer_test.go new file mode 100644 index 00000000000..978fa9fd0d6 --- /dev/null +++ b/pkg/models/jsonschema/performer_test.go @@ -0,0 +1,52 @@ +package jsonschema + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_loadPerformer(t *testing.T) { + tests := []struct { + name string + input string + want Performer + wantErr bool + }{ + { + name: "alias list", + input: ` +{ + "aliases": ["alias1", "alias2"] +}`, + want: Performer{ + Aliases: []string{"alias1", "alias2"}, + }, + wantErr: false, + }, + { + name: "alias string list", + input: ` +{ + "aliases": "alias1, alias2" +}`, + want: Performer{ + Aliases: []string{"alias1", "alias2"}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := strings.NewReader(tt.input) + got, err := loadPerformer(r) + if (err != nil) != tt.wantErr { + t.Errorf("loadPerformer() error = %v, wantErr %v", err, tt.wantErr) + return + } + + assert.Equal(t, &tt.want, got) + }) + } +} diff --git a/pkg/models/mocks/PerformerReaderWriter.go b/pkg/models/mocks/PerformerReaderWriter.go index cf1d965fa39..313ed73bd17 100644 --- a/pkg/models/mocks/PerformerReaderWriter.go +++ b/pkg/models/mocks/PerformerReaderWriter.go @@ -305,6 +305,29 @@ func (_m *PerformerReaderWriter) FindMany(ctx context.Context, ids []int) ([]*mo return r0, r1 } +// GetAliases provides a mock function with given fields: ctx, relatedID +func (_m *PerformerReaderWriter) GetAliases(ctx context.Context, relatedID int) ([]string, error) { + ret := _m.Called(ctx, relatedID) + + var r0 []string + if rf, ok := ret.Get(0).(func(context.Context, int) []string); ok { + r0 = rf(ctx, relatedID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, relatedID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetImage provides a mock function with given fields: ctx, performerID func (_m *PerformerReaderWriter) GetImage(ctx context.Context, performerID int) ([]byte, error) { ret := _m.Called(ctx, performerID) @@ -351,13 +374,13 @@ func (_m *PerformerReaderWriter) GetStashIDs(ctx context.Context, relatedID int) return r0, r1 } -// GetTagIDs provides a mock function with given fields: ctx, performerID -func (_m *PerformerReaderWriter) GetTagIDs(ctx context.Context, performerID int) ([]int, error) { - ret := _m.Called(ctx, performerID) +// GetTagIDs provides a mock function with given fields: ctx, relatedID +func (_m *PerformerReaderWriter) GetTagIDs(ctx context.Context, relatedID int) ([]int, error) { + ret := _m.Called(ctx, relatedID) var r0 []int if rf, ok := ret.Get(0).(func(context.Context, int) []int); ok { - r0 = rf(ctx, performerID) + r0 = rf(ctx, relatedID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]int) @@ -366,7 +389,7 @@ func (_m *PerformerReaderWriter) GetTagIDs(ctx context.Context, performerID int) var r1 error if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { - r1 = rf(ctx, performerID) + r1 = rf(ctx, relatedID) } else { r1 = ret.Error(1) } @@ -477,31 +500,3 @@ func (_m *PerformerReaderWriter) UpdatePartial(ctx context.Context, id int, upda return r0, r1 } - -// UpdateStashIDs provides a mock function with given fields: ctx, performerID, stashIDs -func (_m *PerformerReaderWriter) UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error { - ret := _m.Called(ctx, performerID, stashIDs) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int, []models.StashID) error); ok { - r0 = rf(ctx, performerID, stashIDs) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// UpdateTags provides a mock function with given fields: ctx, performerID, tagIDs -func (_m *PerformerReaderWriter) UpdateTags(ctx context.Context, performerID int, tagIDs []int) error { - ret := _m.Called(ctx, performerID, tagIDs) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int, []int) error); ok { - r0 = rf(ctx, performerID, tagIDs) - } else { - r0 = ret.Error(0) - } - - return r0 -} diff --git a/pkg/models/model_performer.go b/pkg/models/model_performer.go index 12327ac261e..6b626aecc74 100644 --- a/pkg/models/model_performer.go +++ b/pkg/models/model_performer.go @@ -6,37 +6,44 @@ import ( ) type Performer struct { - ID int `json:"id"` - Name string `json:"name"` - Gender GenderEnum `json:"gender"` - URL string `json:"url"` - Twitter string `json:"twitter"` - Instagram string `json:"instagram"` - Birthdate *Date `json:"birthdate"` - Ethnicity string `json:"ethnicity"` - Country string `json:"country"` - EyeColor string `json:"eye_color"` - Height *int `json:"height"` - Measurements string `json:"measurements"` - FakeTits string `json:"fake_tits"` - CareerLength string `json:"career_length"` - Tattoos string `json:"tattoos"` - Piercings string `json:"piercings"` - Aliases string `json:"aliases"` - Favorite bool `json:"favorite"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Rating *int `json:"rating"` - Details string `json:"details"` - DeathDate *Date `json:"death_date"` - HairColor string `json:"hair_color"` - Weight *int `json:"weight"` - IgnoreAutoTag bool `json:"ignore_auto_tag"` + ID int `json:"id"` + Name string `json:"name"` + Disambiguation string `json:"disambiguation"` + Gender GenderEnum `json:"gender"` + URL string `json:"url"` + Twitter string `json:"twitter"` + Instagram string `json:"instagram"` + Birthdate *Date `json:"birthdate"` + Ethnicity string `json:"ethnicity"` + Country string `json:"country"` + EyeColor string `json:"eye_color"` + Height *int `json:"height"` + Measurements string `json:"measurements"` + FakeTits string `json:"fake_tits"` + CareerLength string `json:"career_length"` + Tattoos string `json:"tattoos"` + Piercings string `json:"piercings"` + Favorite bool `json:"favorite"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Rating *int `json:"rating"` + Details string `json:"details"` + DeathDate *Date `json:"death_date"` + HairColor string `json:"hair_color"` + Weight *int `json:"weight"` + IgnoreAutoTag bool `json:"ignore_auto_tag"` + Aliases RelatedStrings `json:"aliases"` TagIDs RelatedIDs `json:"tag_ids"` StashIDs RelatedStashIDs `json:"stash_ids"` } +func (s *Performer) LoadAliases(ctx context.Context, l AliasLoader) error { + return s.Aliases.load(func() ([]string, error) { + return l.GetAliases(ctx, s.ID) + }) +} + func (s *Performer) LoadTagIDs(ctx context.Context, l TagIDLoader) error { return s.TagIDs.load(func() ([]int, error) { return l.GetTagIDs(ctx, s.ID) @@ -50,6 +57,10 @@ func (s *Performer) LoadStashIDs(ctx context.Context, l StashIDLoader) error { } func (s *Performer) LoadRelationships(ctx context.Context, l PerformerReader) error { + if err := s.LoadAliases(ctx, l); err != nil { + return err + } + if err := s.LoadTagIDs(ctx, l); err != nil { return err } @@ -64,33 +75,34 @@ func (s *Performer) LoadRelationships(ctx context.Context, l PerformerReader) er // PerformerPartial represents part of a Performer object. It is used to update // the database entry. type PerformerPartial struct { - ID int - Name OptionalString - Gender OptionalString - URL OptionalString - Twitter OptionalString - Instagram OptionalString - Birthdate OptionalDate - Ethnicity OptionalString - Country OptionalString - EyeColor OptionalString - Height OptionalInt - Measurements OptionalString - FakeTits OptionalString - CareerLength OptionalString - Tattoos OptionalString - Piercings OptionalString - Aliases OptionalString - Favorite OptionalBool - CreatedAt OptionalTime - UpdatedAt OptionalTime - Rating OptionalInt - Details OptionalString - DeathDate OptionalDate - HairColor OptionalString - Weight OptionalInt - IgnoreAutoTag OptionalBool + ID int + Name OptionalString + Disambiguation OptionalString + Gender OptionalString + URL OptionalString + Twitter OptionalString + Instagram OptionalString + Birthdate OptionalDate + Ethnicity OptionalString + Country OptionalString + EyeColor OptionalString + Height OptionalInt + Measurements OptionalString + FakeTits OptionalString + CareerLength OptionalString + Tattoos OptionalString + Piercings OptionalString + Favorite OptionalBool + CreatedAt OptionalTime + UpdatedAt OptionalTime + Rating OptionalInt + Details OptionalString + DeathDate OptionalDate + HairColor OptionalString + Weight OptionalInt + IgnoreAutoTag OptionalBool + Aliases *UpdateStrings TagIDs *UpdateIDs StashIDs *UpdateStashIDs } diff --git a/pkg/models/performer.go b/pkg/models/performer.go index b7d8c72dc20..e72a19b6e06 100644 --- a/pkg/models/performer.go +++ b/pkg/models/performer.go @@ -147,6 +147,7 @@ type PerformerReader interface { // support the query needed QueryForAutoTag(ctx context.Context, words []string) ([]*Performer, error) Query(ctx context.Context, performerFilter *PerformerFilterType, findFilter *FindFilterType) ([]*Performer, int, error) + AliasLoader GetImage(ctx context.Context, performerID int) ([]byte, error) StashIDLoader TagIDLoader diff --git a/pkg/models/relationships.go b/pkg/models/relationships.go index 41bd0a69c4f..504fa8483eb 100644 --- a/pkg/models/relationships.go +++ b/pkg/models/relationships.go @@ -42,6 +42,10 @@ type FileLoader interface { GetFiles(ctx context.Context, relatedID int) ([]file.File, error) } +type AliasLoader interface { + GetAliases(ctx context.Context, relatedID int) ([]string, error) +} + // RelatedIDs represents a list of related IDs. // TODO - this can be made generic type RelatedIDs struct { @@ -468,3 +472,61 @@ func (r *RelatedFiles) loadPrimary(fn func() (file.File, error)) error { return nil } + +// RelatedStrings represents a list of related strings. +// TODO - this can be made generic +type RelatedStrings struct { + list []string +} + +// NewRelatedStrings returns a loaded RelatedStrings object with the provided values. +// Loaded will return true when called on the returned object if the provided slice is not nil. +func NewRelatedStrings(values []string) RelatedStrings { + return RelatedStrings{ + list: values, + } +} + +// Loaded returns true if the related IDs have been loaded. +func (r RelatedStrings) Loaded() bool { + return r.list != nil +} + +func (r RelatedStrings) mustLoaded() { + if !r.Loaded() { + panic("list has not been loaded") + } +} + +// List returns the related values. Panics if the relationship has not been loaded. +func (r RelatedStrings) List() []string { + r.mustLoaded() + + return r.list +} + +// Add adds the provided values to the list. Panics if the relationship has not been loaded. +func (r *RelatedStrings) Add(values ...string) { + r.mustLoaded() + + r.list = append(r.list, values...) +} + +func (r *RelatedStrings) load(fn func() ([]string, error)) error { + if r.Loaded() { + return nil + } + + values, err := fn() + if err != nil { + return err + } + + if values == nil { + values = []string{} + } + + r.list = values + + return nil +} diff --git a/pkg/models/update.go b/pkg/models/update.go index ecc9314ec46..fbfab3d3029 100644 --- a/pkg/models/update.go +++ b/pkg/models/update.go @@ -63,3 +63,8 @@ func (u *UpdateIDs) IDStrings() []string { return intslice.IntSliceToStringSlice(u.IDs) } + +type UpdateStrings struct { + Values []string `json:"values"` + Mode RelationshipUpdateMode `json:"mode"` +} diff --git a/pkg/performer/export.go b/pkg/performer/export.go index 90e50cb6953..e88a77034b1 100644 --- a/pkg/performer/export.go +++ b/pkg/performer/export.go @@ -11,13 +11,14 @@ import ( "github.com/stashapp/stash/pkg/utils" ) -type ImageStashIDGetter interface { +type ImageAliasStashIDGetter interface { GetImage(ctx context.Context, performerID int) ([]byte, error) + models.AliasLoader models.StashIDLoader } // ToJSON converts a Performer object into its JSON equivalent. -func ToJSON(ctx context.Context, reader ImageStashIDGetter, performer *models.Performer) (*jsonschema.Performer, error) { +func ToJSON(ctx context.Context, reader ImageAliasStashIDGetter, performer *models.Performer) (*jsonschema.Performer, error) { newPerformerJSON := jsonschema.Performer{ Name: performer.Name, Gender: performer.Gender.String(), @@ -30,7 +31,6 @@ func ToJSON(ctx context.Context, reader ImageStashIDGetter, performer *models.Pe CareerLength: performer.CareerLength, Tattoos: performer.Tattoos, Piercings: performer.Piercings, - Aliases: performer.Aliases, Twitter: performer.Twitter, Instagram: performer.Instagram, Favorite: performer.Favorite, @@ -59,27 +59,27 @@ func ToJSON(ctx context.Context, reader ImageStashIDGetter, performer *models.Pe newPerformerJSON.Weight = *performer.Weight } + if err := performer.LoadAliases(ctx, reader); err != nil { + return nil, fmt.Errorf("loading performer aliases: %w", err) + } + + newPerformerJSON.Aliases = performer.Aliases.List() + + if err := performer.LoadStashIDs(ctx, reader); err != nil { + return nil, fmt.Errorf("loading performer stash ids: %w", err) + } + + newPerformerJSON.StashIDs = performer.StashIDs.List() + image, err := reader.GetImage(ctx, performer.ID) if err != nil { - return nil, fmt.Errorf("error getting performers image: %v", err) + return nil, fmt.Errorf("getting performers image: %w", err) } if len(image) > 0 { newPerformerJSON.Image = utils.GetBase64StringFromData(image) } - stashIDs, _ := reader.GetStashIDs(ctx, performer.ID) - var ret []models.StashID - for _, stashID := range stashIDs { - newJoin := models.StashID{ - StashID: stashID.StashID, - Endpoint: stashID.Endpoint, - } - ret = append(ret, newJoin) - } - - newPerformerJSON.StashIDs = ret - return &newPerformerJSON, nil } diff --git a/pkg/performer/export_test.go b/pkg/performer/export_test.go index 312db1930bd..a5b990bf330 100644 --- a/pkg/performer/export_test.go +++ b/pkg/performer/export_test.go @@ -23,7 +23,6 @@ const ( const ( performerName = "testPerformer" url = "url" - aliases = "aliases" careerLength = "careerLength" country = "country" ethnicity = "ethnicity" @@ -42,9 +41,10 @@ const ( ) var ( - rating = 5 - height = 123 - weight = 60 + aliases = []string{"alias1", "alias2"} + rating = 5 + height = 123 + weight = 60 ) var imageBytes = []byte("imageBytes") @@ -72,7 +72,7 @@ func createFullPerformer(id int, name string) *models.Performer { ID: id, Name: name, URL: url, - Aliases: aliases, + Aliases: models.NewRelatedStrings(aliases), Birthdate: &birthDate, CareerLength: careerLength, Country: country, @@ -96,9 +96,7 @@ func createFullPerformer(id int, name string) *models.Performer { Weight: &weight, IgnoreAutoTag: autoTagIgnored, TagIDs: models.NewRelatedIDs([]int{}), - StashIDs: models.NewRelatedStashIDs([]models.StashID{ - stashID, - }), + StashIDs: models.NewRelatedStashIDs(stashIDs), } } @@ -107,6 +105,9 @@ func createEmptyPerformer(id int) models.Performer { ID: id, CreatedAt: createTime, UpdatedAt: updateTime, + Aliases: models.NewRelatedStrings([]string{}), + TagIDs: models.NewRelatedIDs([]int{}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{}), } } @@ -135,21 +136,21 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer { UpdatedAt: json.JSONTime{ Time: updateTime, }, - Rating: rating, - Image: image, - Details: details, - DeathDate: deathDate.String(), - HairColor: hairColor, - Weight: weight, - StashIDs: []models.StashID{ - stashID, - }, + Rating: rating, + Image: image, + Details: details, + DeathDate: deathDate.String(), + HairColor: hairColor, + Weight: weight, + StashIDs: stashIDs, IgnoreAutoTag: autoTagIgnored, } } func createEmptyJSONPerformer() *jsonschema.Performer { return &jsonschema.Performer{ + Aliases: []string{}, + StashIDs: []models.StashID{}, CreatedAt: json.JSONTime{ Time: createTime, }, @@ -198,9 +199,6 @@ func TestToJSON(t *testing.T) { mockPerformerReader.On("GetImage", testCtx, noImageID).Return(nil, nil).Once() mockPerformerReader.On("GetImage", testCtx, errImageID).Return(nil, imageErr).Once() - mockPerformerReader.On("GetStashIDs", testCtx, performerID).Return(stashIDs, nil).Once() - mockPerformerReader.On("GetStashIDs", testCtx, noImageID).Return(nil, nil).Once() - for i, s := range scenarios { tag := s.input json, err := ToJSON(testCtx, mockPerformerReader, &tag) diff --git a/pkg/performer/import.go b/pkg/performer/import.go index 10bfd53836b..0f202a607c3 100644 --- a/pkg/performer/import.go +++ b/pkg/performer/import.go @@ -179,7 +179,7 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform CareerLength: performerJSON.CareerLength, Tattoos: performerJSON.Tattoos, Piercings: performerJSON.Piercings, - Aliases: performerJSON.Aliases, + Aliases: models.NewRelatedStrings(performerJSON.Aliases), Twitter: performerJSON.Twitter, Instagram: performerJSON.Instagram, Details: performerJSON.Details, diff --git a/pkg/scraper/stashbox/stash_box.go b/pkg/scraper/stashbox/stash_box.go index 1f78203788e..a0d4767a3bc 100644 --- a/pkg/scraper/stashbox/stash_box.go +++ b/pkg/scraper/stashbox/stash_box.go @@ -43,6 +43,7 @@ type PerformerReader interface { match.PerformerFinder Find(ctx context.Context, id int) (*models.Performer, error) FindBySceneID(ctx context.Context, sceneID int) ([]*models.Performer, error) + models.AliasLoader models.StashIDLoader GetImage(ctx context.Context, performerID int) ([]byte, error) } @@ -944,6 +945,15 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf draft := graphql.PerformerDraftInput{} var image io.Reader pqb := c.repository.Performer + + if err := performer.LoadAliases(ctx, pqb); err != nil { + return nil, err + } + + if err := performer.LoadStashIDs(ctx, pqb); err != nil { + return nil, err + } + img, _ := pqb.GetImage(ctx, performer.ID) if img != nil { image = bytes.NewReader(img) @@ -988,8 +998,9 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf if performer.Tattoos != "" { draft.Tattoos = &performer.Tattoos } - if performer.Aliases != "" { - draft.Aliases = &performer.Aliases + if len(performer.Aliases.List()) > 0 { + aliases := strings.Join(performer.Aliases.List(), ",") + draft.Aliases = &aliases } var urls []string diff --git a/pkg/sliceutil/stringslice/string_collections.go b/pkg/sliceutil/stringslice/string_collections.go index f466d911b20..3c66c5c4847 100644 --- a/pkg/sliceutil/stringslice/string_collections.go +++ b/pkg/sliceutil/stringslice/string_collections.go @@ -1,6 +1,9 @@ package stringslice -import "strconv" +import ( + "strconv" + "strings" +) // https://gobyexample.com/collection-functions @@ -56,6 +59,19 @@ func StrAppendUniques(vs []string, toAdd []string) []string { return vs } +// StrExclude removes all instances of any value in toExclude from the vs string +// slice. It returns the new or unchanged string slice. +func StrExclude(vs []string, toExclude []string) []string { + var ret []string + for _, v := range vs { + if !StrInclude(toExclude, v) { + ret = append(ret, v) + } + } + + return ret +} + // StrUnique returns the vs string slice with non-unique values removed. func StrUnique(vs []string) []string { distinctValues := make(map[string]struct{}) @@ -94,3 +110,13 @@ func StringSliceToIntSlice(ss []string) ([]int, error) { return ret, nil } + +// FromString converts a string to a slice of strings, splitting on the sep character. +// Unlike strings.Split, this function will also trim whitespace from the resulting strings. +func FromString(s string, sep string) []string { + v := strings.Split(s, ",") + for i, vv := range v { + v[i] = strings.TrimSpace(vv) + } + return v +} diff --git a/pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql b/pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql index 16ed777b673..49672b84164 100644 --- a/pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql +++ b/pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql @@ -1,2 +1,16 @@ +CREATE TABLE `performer_aliases` ( + `performer_id` integer NOT NULL, + `alias` varchar(255) NOT NULL, + foreign key(`performer_id`) references `performers`(`id`) on delete CASCADE, + PRIMARY KEY(`performer_id`, `alias`) +); + +CREATE INDEX `performer_aliases_alias` on `performer_aliases` (`alias`); + +-- `performers`.`aliases` will be dropped in the post-migration + DROP INDEX `performers_checksum_unique`; ALTER TABLE `performers` DROP COLUMN `checksum`; +ALTER TABLE `performers` ADD COLUMN `disambiguation` varchar(255); + +CREATE UNIQUE INDEX `performers_name_disambiguation` on `performers` (`name`, `disambiguation`); diff --git a/pkg/sqlite/migrations/39_postmigrate.go b/pkg/sqlite/migrations/39_postmigrate.go new file mode 100644 index 00000000000..671e365ef30 --- /dev/null +++ b/pkg/sqlite/migrations/39_postmigrate.go @@ -0,0 +1,130 @@ +package migrations + +import ( + "context" + "fmt" + "strings" + + "github.com/jmoiron/sqlx" + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/sliceutil/stringslice" + "github.com/stashapp/stash/pkg/sqlite" +) + +type schema39Migrator struct { + migrator +} + +func post39(ctx context.Context, db *sqlx.DB) error { + logger.Info("Running post-migration for schema version 39") + + m := schema39Migrator{ + migrator: migrator{ + db: db, + }, + } + + if err := m.migrate(ctx); err != nil { + return fmt.Errorf("migrating performer aliases: %w", err) + } + + return nil +} + +func (m *schema39Migrator) migrate(ctx context.Context) error { + logger.Info("Migrating performer aliases") + + const ( + limit = 1000 + logEvery = 10000 + ) + + lastID := 0 + count := 0 + + for { + gotSome := false + + if err := m.withTxn(ctx, func(tx *sqlx.Tx) error { + query := "SELECT `id`, `aliases` FROM `performers` WHERE `aliases` IS NOT NULL AND `aliases` != ''" + + if lastID != 0 { + query += fmt.Sprintf(" AND `id` > %d ", lastID) + } + + query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit) + + rows, err := m.db.Query(query) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var ( + id int + aliases string + ) + + err := rows.Scan(id, aliases) + if err != nil { + return err + } + + lastID = id + gotSome = true + count++ + + if err := m.migratePerformerAliases(id, aliases); err != nil { + return err + } + } + + return rows.Err() + }); err != nil { + return err + } + + if !gotSome { + break + } + + if count%logEvery == 0 { + logger.Infof("Migrated %d rows", count) + } + } + + // drop the aliases column + if _, err := m.db.Exec("ALTER TABLE `performers` DROP COLUMN `aliases`"); err != nil { + return err + } + + return nil +} + +func (m *schema39Migrator) migratePerformerAliases(id int, aliases string) error { + // split aliases by comma + aliasList := strings.Split(aliases, ",") + + // trim whitespace from each alias + for i, alias := range aliasList { + aliasList[i] = strings.TrimSpace(alias) + } + + // remove duplicates + aliasList = stringslice.StrAppendUniques(nil, aliasList) + + // insert aliases into table + for _, alias := range aliasList { + _, err := m.db.Exec("INSERT INTO `performer_aliases` (`performer_id`, `alias`) VALUES (?, ?)", id, alias) + if err != nil { + return err + } + } + + return nil +} + +func init() { + sqlite.RegisterPostMigration(39, post39) +} diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index c13ac4beadf..dde8c535bb5 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -17,14 +17,19 @@ import ( "gopkg.in/guregu/null.v4/zero" ) -const performerTable = "performers" -const performerIDColumn = "performer_id" -const performersTagsTable = "performers_tags" -const performersImageTable = "performers_image" // performer cover image +const ( + performerTable = "performers" + performerIDColumn = "performer_id" + performersAliasesTable = "performer_aliases" + performerAliasColumn = "alias" + performersTagsTable = "performers_tags" + performersImageTable = "performers_image" // performer cover image +) type performerRow struct { ID int `db:"id" goqu:"skipinsert"` Name string `db:"name"` + Disambigation zero.String `db:"disambiguation"` Gender zero.String `db:"gender"` URL zero.String `db:"url"` Twitter zero.String `db:"twitter"` @@ -39,7 +44,6 @@ type performerRow struct { CareerLength zero.String `db:"career_length"` Tattoos zero.String `db:"tattoos"` Piercings zero.String `db:"piercings"` - Aliases zero.String `db:"aliases"` Favorite sql.NullBool `db:"favorite"` CreatedAt models.SQLiteTimestamp `db:"created_at"` UpdatedAt models.SQLiteTimestamp `db:"updated_at"` @@ -54,6 +58,7 @@ type performerRow struct { func (r *performerRow) fromPerformer(o models.Performer) { r.ID = o.ID r.Name = o.Name + r.Disambigation = zero.StringFrom(o.Disambiguation) if o.Gender.IsValid() { r.Gender = zero.StringFrom(o.Gender.String()) } @@ -72,7 +77,6 @@ func (r *performerRow) fromPerformer(o models.Performer) { r.CareerLength = zero.StringFrom(o.CareerLength) r.Tattoos = zero.StringFrom(o.Tattoos) r.Piercings = zero.StringFrom(o.Piercings) - r.Aliases = zero.StringFrom(o.Aliases) r.Favorite = sql.NullBool{Bool: o.Favorite, Valid: true} r.CreatedAt = models.SQLiteTimestamp{Timestamp: o.CreatedAt} r.UpdatedAt = models.SQLiteTimestamp{Timestamp: o.UpdatedAt} @@ -88,32 +92,32 @@ func (r *performerRow) fromPerformer(o models.Performer) { func (r *performerRow) resolve() *models.Performer { ret := &models.Performer{ - ID: r.ID, - Name: r.Name, - Gender: models.GenderEnum(r.Gender.String), - URL: r.URL.String, - Twitter: r.Twitter.String, - Instagram: r.Instagram.String, - Birthdate: r.Birthdate.DatePtr(), - Ethnicity: r.Ethnicity.String, - Country: r.Country.String, - EyeColor: r.EyeColor.String, - Height: nullIntPtr(r.Height), - Measurements: r.Measurements.String, - FakeTits: r.FakeTits.String, - CareerLength: r.CareerLength.String, - Tattoos: r.Tattoos.String, - Piercings: r.Piercings.String, - Aliases: r.Aliases.String, - Favorite: r.Favorite.Bool, - CreatedAt: r.CreatedAt.Timestamp, - UpdatedAt: r.UpdatedAt.Timestamp, - Rating: nullIntPtr(r.Rating), - Details: r.Details.String, - DeathDate: r.DeathDate.DatePtr(), - HairColor: r.HairColor.String, - Weight: nullIntPtr(r.Weight), - IgnoreAutoTag: r.IgnoreAutoTag, + ID: r.ID, + Name: r.Name, + Disambiguation: r.Disambigation.String, + Gender: models.GenderEnum(r.Gender.String), + URL: r.URL.String, + Twitter: r.Twitter.String, + Instagram: r.Instagram.String, + Birthdate: r.Birthdate.DatePtr(), + Ethnicity: r.Ethnicity.String, + Country: r.Country.String, + EyeColor: r.EyeColor.String, + Height: nullIntPtr(r.Height), + Measurements: r.Measurements.String, + FakeTits: r.FakeTits.String, + CareerLength: r.CareerLength.String, + Tattoos: r.Tattoos.String, + Piercings: r.Piercings.String, + Favorite: r.Favorite.Bool, + CreatedAt: r.CreatedAt.Timestamp, + UpdatedAt: r.UpdatedAt.Timestamp, + Rating: nullIntPtr(r.Rating), + Details: r.Details.String, + DeathDate: r.DeathDate.DatePtr(), + HairColor: r.HairColor.String, + Weight: nullIntPtr(r.Weight), + IgnoreAutoTag: r.IgnoreAutoTag, } return ret @@ -125,6 +129,7 @@ type performerRowRecord struct { func (r *performerRowRecord) fromPartial(o models.PerformerPartial) { r.setString("name", o.Name) + r.setNullString("disambiguation", o.Disambiguation) r.setNullString("gender", o.Gender) r.setNullString("url", o.URL) r.setNullString("twitter", o.Twitter) @@ -139,7 +144,6 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) { r.setNullString("career_length", o.CareerLength) r.setNullString("tattoos", o.Tattoos) r.setNullString("piercings", o.Piercings) - r.setNullString("aliases", o.Aliases) r.setBool("favorite", o.Favorite) r.setSQLiteTimestamp("created_at", o.CreatedAt) r.setSQLiteTimestamp("updated_at", o.UpdatedAt) @@ -176,6 +180,12 @@ func (qb *PerformerStore) Create(ctx context.Context, newObject *models.Performe return err } + if newObject.Aliases.Loaded() { + if err := performersAliasesTableMgr.insertJoins(ctx, id, newObject.Aliases.List()); err != nil { + return err + } + } + if newObject.TagIDs.Loaded() { if err := performersTagsTableMgr.insertJoins(ctx, id, newObject.TagIDs.List()); err != nil { return err @@ -213,6 +223,12 @@ func (qb *PerformerStore) UpdatePartial(ctx context.Context, id int, partial mod } } + if partial.Aliases != nil { + if err := performersAliasesTableMgr.modifyJoins(ctx, id, partial.Aliases.Values, partial.Aliases.Mode); err != nil { + return nil, err + } + } + if partial.TagIDs != nil { if err := performersTagsTableMgr.modifyJoins(ctx, id, partial.TagIDs.IDs, partial.TagIDs.Mode); err != nil { return nil, err @@ -235,6 +251,12 @@ func (qb *PerformerStore) Update(ctx context.Context, updatedObject *models.Perf return err } + if updatedObject.Aliases.Loaded() { + if err := performersAliasesTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.Aliases.List()); err != nil { + return err + } + } + if updatedObject.TagIDs.Loaded() { if err := performersTagsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.TagIDs.List()); err != nil { return err @@ -561,8 +583,7 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform } })) - // TODO - need better handling of aliases - query.handleCriterion(ctx, stringCriterionHandler(filter.Aliases, tableName+".aliases")) + query.handleCriterion(ctx, performerAliasCriterionHandler(qb, filter.Aliases)) query.handleCriterion(ctx, performerTagsCriterionHandler(qb, filter.Tags)) @@ -588,7 +609,8 @@ func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.Per distinctIDs(&query, performerTable) if q := findFilter.Q; q != nil && *q != "" { - searchColumns := []string{"performers.name", "performers.aliases"} + query.join(performersAliasesTable, "", "performer_aliases.performer_id = performers.id") + searchColumns := []string{"performers.name", "performer_aliases.alias"} query.parseQueryString(searchColumns, *q) } @@ -654,6 +676,18 @@ func performerAgeFilterCriterionHandler(age *models.IntCriterionInput) criterion } } +func performerAliasCriterionHandler(qb *PerformerStore, alias *models.StringCriterionInput) criterionHandlerFunc { + h := stringListCriterionHandlerBuilder{ + joinTable: performersAliasesTable, + stringColumn: performerAliasColumn, + addJoinTable: func(f *filterBuilder) { + performersAliasesTableMgr.join(f, "", "tags.id") + }, + } + + return h.handler(alias) +} + func performerTagsCriterionHandler(qb *PerformerStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { h := joinedHierarchicalMultiCriterionHandlerBuilder{ tx: qb.tx, @@ -853,6 +887,10 @@ func (qb *PerformerStore) DestroyImage(ctx context.Context, performerID int) err return qb.imageRepository().destroy(ctx, []int{performerID}) } +func (qb *PerformerStore) GetAliases(ctx context.Context, performerID int) ([]string, error) { + return performersAliasesTableMgr.get(ctx, performerID) +} + func (qb *PerformerStore) GetStashIDs(ctx context.Context, performerID int) ([]models.StashID, error) { return performersStashIDsTableMgr.get(ctx, performerID) } diff --git a/pkg/sqlite/performer_test.go b/pkg/sqlite/performer_test.go index e85725f445a..b6945cb2cb2 100644 --- a/pkg/sqlite/performer_test.go +++ b/pkg/sqlite/performer_test.go @@ -17,6 +17,11 @@ import ( ) func loadPerformerRelationships(ctx context.Context, expected models.Performer, actual *models.Performer) error { + if expected.Aliases.Loaded() { + if err := actual.LoadAliases(ctx, db.Performer); err != nil { + return err + } + } if expected.TagIDs.Loaded() { if err := actual.LoadTagIDs(ctx, db.Performer); err != nil { return err @@ -33,33 +38,34 @@ func loadPerformerRelationships(ctx context.Context, expected models.Performer, func Test_PerformerStore_Create(t *testing.T) { var ( - name = "name" - gender = models.GenderEnumFemale - details = "details" - url = "url" - twitter = "twitter" - instagram = "instagram" - rating = 3 - ethnicity = "ethnicity" - country = "country" - eyeColor = "eyeColor" - height = 134 - measurements = "measurements" - fakeTits = "fakeTits" - careerLength = "careerLength" - tattoos = "tattoos" - piercings = "piercings" - aliases = "aliases" - hairColor = "hairColor" - weight = 123 - ignoreAutoTag = true - favorite = true - endpoint1 = "endpoint1" - endpoint2 = "endpoint2" - stashID1 = "stashid1" - stashID2 = "stashid2" - createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) - updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + name = "name" + disambiguation = "disambiguation" + gender = models.GenderEnumFemale + details = "details" + url = "url" + twitter = "twitter" + instagram = "instagram" + rating = 3 + ethnicity = "ethnicity" + country = "country" + eyeColor = "eyeColor" + height = 134 + measurements = "measurements" + fakeTits = "fakeTits" + careerLength = "careerLength" + tattoos = "tattoos" + piercings = "piercings" + aliases = []string{"alias1", "alias2"} + hairColor = "hairColor" + weight = 123 + ignoreAutoTag = true + favorite = true + endpoint1 = "endpoint1" + endpoint2 = "endpoint2" + stashID1 = "stashid1" + stashID2 = "stashid2" + createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) birthdate = models.NewDate("2003-02-01") deathdate = models.NewDate("2023-02-01") @@ -73,30 +79,31 @@ func Test_PerformerStore_Create(t *testing.T) { { "full", models.Performer{ - Name: name, - Gender: gender, - URL: url, - Twitter: twitter, - Instagram: instagram, - Birthdate: &birthdate, - Ethnicity: ethnicity, - Country: country, - EyeColor: eyeColor, - Height: &height, - Measurements: measurements, - FakeTits: fakeTits, - CareerLength: careerLength, - Tattoos: tattoos, - Piercings: piercings, - Aliases: aliases, - Favorite: favorite, - Rating: &rating, - Details: details, - DeathDate: &deathdate, - HairColor: hairColor, - Weight: &weight, - IgnoreAutoTag: ignoreAutoTag, - TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}), + Name: name, + Disambiguation: disambiguation, + Gender: gender, + URL: url, + Twitter: twitter, + Instagram: instagram, + Birthdate: &birthdate, + Ethnicity: ethnicity, + Country: country, + EyeColor: eyeColor, + Height: &height, + Measurements: measurements, + FakeTits: fakeTits, + CareerLength: careerLength, + Tattoos: tattoos, + Piercings: piercings, + Favorite: favorite, + Rating: &rating, + Details: details, + DeathDate: &deathdate, + HairColor: hairColor, + Weight: &weight, + IgnoreAutoTag: ignoreAutoTag, + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}), + Aliases: models.NewRelatedStrings(aliases), StashIDs: models.NewRelatedStashIDs([]models.StashID{ { StashID: stashID1, @@ -175,33 +182,34 @@ func Test_PerformerStore_Create(t *testing.T) { func Test_PerformerStore_Update(t *testing.T) { var ( - name = "name" - gender = models.GenderEnumFemale - details = "details" - url = "url" - twitter = "twitter" - instagram = "instagram" - rating = 3 - ethnicity = "ethnicity" - country = "country" - eyeColor = "eyeColor" - height = 134 - measurements = "measurements" - fakeTits = "fakeTits" - careerLength = "careerLength" - tattoos = "tattoos" - piercings = "piercings" - aliases = "aliases" - hairColor = "hairColor" - weight = 123 - ignoreAutoTag = true - favorite = true - endpoint1 = "endpoint1" - endpoint2 = "endpoint2" - stashID1 = "stashid1" - stashID2 = "stashid2" - createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) - updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + name = "name" + disambiguation = "disambiguation" + gender = models.GenderEnumFemale + details = "details" + url = "url" + twitter = "twitter" + instagram = "instagram" + rating = 3 + ethnicity = "ethnicity" + country = "country" + eyeColor = "eyeColor" + height = 134 + measurements = "measurements" + fakeTits = "fakeTits" + careerLength = "careerLength" + tattoos = "tattoos" + piercings = "piercings" + aliases = []string{"alias1", "alias2"} + hairColor = "hairColor" + weight = 123 + ignoreAutoTag = true + favorite = true + endpoint1 = "endpoint1" + endpoint2 = "endpoint2" + stashID1 = "stashid1" + stashID2 = "stashid2" + createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) birthdate = models.NewDate("2003-02-01") deathdate = models.NewDate("2023-02-01") @@ -215,31 +223,32 @@ func Test_PerformerStore_Update(t *testing.T) { { "full", &models.Performer{ - ID: performerIDs[performerIdxWithGallery], - Name: name, - Gender: gender, - URL: url, - Twitter: twitter, - Instagram: instagram, - Birthdate: &birthdate, - Ethnicity: ethnicity, - Country: country, - EyeColor: eyeColor, - Height: &height, - Measurements: measurements, - FakeTits: fakeTits, - CareerLength: careerLength, - Tattoos: tattoos, - Piercings: piercings, - Aliases: aliases, - Favorite: favorite, - Rating: &rating, - Details: details, - DeathDate: &deathdate, - HairColor: hairColor, - Weight: &weight, - IgnoreAutoTag: ignoreAutoTag, - TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}), + ID: performerIDs[performerIdxWithGallery], + Name: name, + Disambiguation: disambiguation, + Gender: gender, + URL: url, + Twitter: twitter, + Instagram: instagram, + Birthdate: &birthdate, + Ethnicity: ethnicity, + Country: country, + EyeColor: eyeColor, + Height: &height, + Measurements: measurements, + FakeTits: fakeTits, + CareerLength: careerLength, + Tattoos: tattoos, + Piercings: piercings, + Favorite: favorite, + Rating: &rating, + Details: details, + DeathDate: &deathdate, + HairColor: hairColor, + Weight: &weight, + IgnoreAutoTag: ignoreAutoTag, + Aliases: models.NewRelatedStrings(aliases), + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}), StashIDs: models.NewRelatedStashIDs([]models.StashID{ { StashID: stashID1, @@ -259,6 +268,7 @@ func Test_PerformerStore_Update(t *testing.T) { "clear nullables", &models.Performer{ ID: performerIDs[performerIdxWithGallery], + Aliases: models.NewRelatedStrings([]string{}), TagIDs: models.NewRelatedIDs([]int{}), StashIDs: models.NewRelatedStashIDs([]models.StashID{}), }, @@ -320,60 +330,62 @@ func clearPerformerPartial() models.PerformerPartial { // leave mandatory fields return models.PerformerPartial{ - Gender: nullString, - URL: nullString, - Twitter: nullString, - Instagram: nullString, - Birthdate: nullDate, - Ethnicity: nullString, - Country: nullString, - EyeColor: nullString, - Height: nullInt, - Measurements: nullString, - FakeTits: nullString, - CareerLength: nullString, - Tattoos: nullString, - Piercings: nullString, - Aliases: nullString, - Rating: nullInt, - Details: nullString, - DeathDate: nullDate, - HairColor: nullString, - Weight: nullInt, - TagIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, - StashIDs: &models.UpdateStashIDs{Mode: models.RelationshipUpdateModeSet}, + Disambiguation: nullString, + Gender: nullString, + URL: nullString, + Twitter: nullString, + Instagram: nullString, + Birthdate: nullDate, + Ethnicity: nullString, + Country: nullString, + EyeColor: nullString, + Height: nullInt, + Measurements: nullString, + FakeTits: nullString, + CareerLength: nullString, + Tattoos: nullString, + Piercings: nullString, + Aliases: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet}, + Rating: nullInt, + Details: nullString, + DeathDate: nullDate, + HairColor: nullString, + Weight: nullInt, + TagIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, + StashIDs: &models.UpdateStashIDs{Mode: models.RelationshipUpdateModeSet}, } } func Test_PerformerStore_UpdatePartial(t *testing.T) { var ( - name = "name" - gender = models.GenderEnumFemale - details = "details" - url = "url" - twitter = "twitter" - instagram = "instagram" - rating = 3 - ethnicity = "ethnicity" - country = "country" - eyeColor = "eyeColor" - height = 143 - measurements = "measurements" - fakeTits = "fakeTits" - careerLength = "careerLength" - tattoos = "tattoos" - piercings = "piercings" - aliases = "aliases" - hairColor = "hairColor" - weight = 123 - ignoreAutoTag = true - favorite = true - endpoint1 = "endpoint1" - endpoint2 = "endpoint2" - stashID1 = "stashid1" - stashID2 = "stashid2" - createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) - updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + name = "name" + disambiguation = "disambiguation" + gender = models.GenderEnumFemale + details = "details" + url = "url" + twitter = "twitter" + instagram = "instagram" + rating = 3 + ethnicity = "ethnicity" + country = "country" + eyeColor = "eyeColor" + height = 143 + measurements = "measurements" + fakeTits = "fakeTits" + careerLength = "careerLength" + tattoos = "tattoos" + piercings = "piercings" + aliases = []string{"alias1", "alias2"} + hairColor = "hairColor" + weight = 123 + ignoreAutoTag = true + favorite = true + endpoint1 = "endpoint1" + endpoint2 = "endpoint2" + stashID1 = "stashid1" + stashID2 = "stashid2" + createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) birthdate = models.NewDate("2003-02-01") deathdate = models.NewDate("2023-02-01") @@ -390,22 +402,26 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { "full", performerIDs[performerIdxWithDupName], models.PerformerPartial{ - Name: models.NewOptionalString(name), - Gender: models.NewOptionalString(gender.String()), - URL: models.NewOptionalString(url), - Twitter: models.NewOptionalString(twitter), - Instagram: models.NewOptionalString(instagram), - Birthdate: models.NewOptionalDate(birthdate), - Ethnicity: models.NewOptionalString(ethnicity), - Country: models.NewOptionalString(country), - EyeColor: models.NewOptionalString(eyeColor), - Height: models.NewOptionalInt(height), - Measurements: models.NewOptionalString(measurements), - FakeTits: models.NewOptionalString(fakeTits), - CareerLength: models.NewOptionalString(careerLength), - Tattoos: models.NewOptionalString(tattoos), - Piercings: models.NewOptionalString(piercings), - Aliases: models.NewOptionalString(aliases), + Name: models.NewOptionalString(name), + Disambiguation: models.NewOptionalString(disambiguation), + Gender: models.NewOptionalString(gender.String()), + URL: models.NewOptionalString(url), + Twitter: models.NewOptionalString(twitter), + Instagram: models.NewOptionalString(instagram), + Birthdate: models.NewOptionalDate(birthdate), + Ethnicity: models.NewOptionalString(ethnicity), + Country: models.NewOptionalString(country), + EyeColor: models.NewOptionalString(eyeColor), + Height: models.NewOptionalInt(height), + Measurements: models.NewOptionalString(measurements), + FakeTits: models.NewOptionalString(fakeTits), + CareerLength: models.NewOptionalString(careerLength), + Tattoos: models.NewOptionalString(tattoos), + Piercings: models.NewOptionalString(piercings), + Aliases: &models.UpdateStrings{ + Values: aliases, + Mode: models.RelationshipUpdateModeSet, + }, Favorite: models.NewOptionalBool(favorite), Rating: models.NewOptionalInt(rating), Details: models.NewOptionalString(details), @@ -434,31 +450,32 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { UpdatedAt: models.NewOptionalTime(updatedAt), }, models.Performer{ - ID: performerIDs[performerIdxWithDupName], - Name: name, - Gender: gender, - URL: url, - Twitter: twitter, - Instagram: instagram, - Birthdate: &birthdate, - Ethnicity: ethnicity, - Country: country, - EyeColor: eyeColor, - Height: &height, - Measurements: measurements, - FakeTits: fakeTits, - CareerLength: careerLength, - Tattoos: tattoos, - Piercings: piercings, - Aliases: aliases, - Favorite: favorite, - Rating: &rating, - Details: details, - DeathDate: &deathdate, - HairColor: hairColor, - Weight: &weight, - IgnoreAutoTag: ignoreAutoTag, - TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}), + ID: performerIDs[performerIdxWithDupName], + Name: name, + Disambiguation: disambiguation, + Gender: gender, + URL: url, + Twitter: twitter, + Instagram: instagram, + Birthdate: &birthdate, + Ethnicity: ethnicity, + Country: country, + EyeColor: eyeColor, + Height: &height, + Measurements: measurements, + FakeTits: fakeTits, + CareerLength: careerLength, + Tattoos: tattoos, + Piercings: piercings, + Aliases: models.NewRelatedStrings(aliases), + Favorite: favorite, + Rating: &rating, + Details: details, + DeathDate: &deathdate, + HairColor: hairColor, + Weight: &weight, + IgnoreAutoTag: ignoreAutoTag, + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}), StashIDs: models.NewRelatedStashIDs([]models.StashID{ { StashID: stashID1, @@ -482,6 +499,7 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { ID: performerIDs[performerIdxWithTwoTags], Name: getPerformerStringValue(performerIdxWithTwoTags, "Name"), Favorite: true, + Aliases: models.NewRelatedStrings([]string{}), TagIDs: models.NewRelatedIDs([]int{}), StashIDs: models.NewRelatedStashIDs([]models.StashID{}), }, @@ -1111,6 +1129,7 @@ func verifyPerformerQuery(t *testing.T, filter models.PerformerFilterType, verif } func queryPerformers(ctx context.Context, t *testing.T, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) []*models.Performer { + t.Helper() performers, _, err := db.Performer.Query(ctx, performerFilter, findFilter) if err != nil { t.Errorf("Error querying performers: %s", err.Error()) diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index 0c384ecf3ec..eb4c0b0b7cb 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -1297,16 +1297,18 @@ func createPerformers(ctx context.Context, n int, o int) error { tids := indexesToIDs(tagIDs, performerTags[i]) performer := models.Performer{ - Name: getPerformerStringValue(index, name), - URL: getPerformerNullStringValue(i, urlField), - Favorite: getPerformerBoolValue(i), - Birthdate: getPerformerBirthdate(i), - DeathDate: getPerformerDeathDate(i), - Details: getPerformerStringValue(i, "Details"), - Ethnicity: getPerformerStringValue(i, "Ethnicity"), - Rating: getIntPtr(getRating(i)), - IgnoreAutoTag: getIgnoreAutoTag(i), - TagIDs: models.NewRelatedIDs(tids), + Name: getPerformerStringValue(index, name), + Disambiguation: getPerformerStringValue(index, "disambiguation"), + Aliases: models.NewRelatedStrings([]string{getPerformerStringValue(index, "alias")}), + URL: getPerformerNullStringValue(i, urlField), + Favorite: getPerformerBoolValue(i), + Birthdate: getPerformerBirthdate(i), + DeathDate: getPerformerDeathDate(i), + Details: getPerformerStringValue(i, "Details"), + Ethnicity: getPerformerStringValue(i, "Ethnicity"), + Rating: getIntPtr(getRating(i)), + IgnoreAutoTag: getIgnoreAutoTag(i), + TagIDs: models.NewRelatedIDs(tids), } careerLength := getPerformerCareerLength(i) diff --git a/pkg/sqlite/table.go b/pkg/sqlite/table.go index 11f21d7b4d3..e7a613a6f0e 100644 --- a/pkg/sqlite/table.go +++ b/pkg/sqlite/table.go @@ -15,6 +15,7 @@ import ( "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/sliceutil/intslice" + "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) type table struct { @@ -375,6 +376,102 @@ func (t *stashIDTable) modifyJoins(ctx context.Context, id int, v []models.Stash return nil } +type stringTable struct { + table + stringColumn exp.IdentifierExpression +} + +func (t *stringTable) get(ctx context.Context, id int) ([]string, error) { + q := dialect.Select(t.stringColumn).From(t.table.table).Where(t.idColumn.Eq(id)) + + const single = false + var ret []string + if err := queryFunc(ctx, q, single, func(rows *sqlx.Rows) error { + var v string + if err := rows.Scan(&v); err != nil { + return err + } + + ret = append(ret, v) + + return nil + }); err != nil { + return nil, fmt.Errorf("getting stash ids from %s: %w", t.table.table.GetTable(), err) + } + + return ret, nil +} + +func (t *stringTable) insertJoin(ctx context.Context, id int, v string) (sql.Result, error) { + q := dialect.Insert(t.table.table).Cols(t.idColumn.GetCol(), t.stringColumn.GetCol()).Vals( + goqu.Vals{id, v}, + ) + ret, err := exec(ctx, q) + if err != nil { + return nil, fmt.Errorf("inserting into %s: %w", t.table.table.GetTable(), err) + } + + return ret, nil +} + +func (t *stringTable) insertJoins(ctx context.Context, id int, v []string) error { + for _, fk := range v { + if _, err := t.insertJoin(ctx, id, fk); err != nil { + return err + } + } + + return nil +} + +func (t *stringTable) replaceJoins(ctx context.Context, id int, v []string) error { + if err := t.destroy(ctx, []int{id}); err != nil { + return err + } + + return t.insertJoins(ctx, id, v) +} + +func (t *stringTable) addJoins(ctx context.Context, id int, v []string) error { + // get existing foreign keys + existing, err := t.get(ctx, id) + if err != nil { + return err + } + + // only add values that are not already present + filtered := stringslice.StrExclude(v, existing) + return t.insertJoins(ctx, id, filtered) +} + +func (t *stringTable) destroyJoins(ctx context.Context, id int, v []string) error { + for _, vv := range v { + q := dialect.Delete(t.table.table).Where( + t.idColumn.Eq(id), + t.stringColumn.Eq(vv), + ) + + if _, err := exec(ctx, q); err != nil { + return fmt.Errorf("destroying %s: %w", t.table.table.GetTable(), err) + } + } + + return nil +} + +func (t *stringTable) modifyJoins(ctx context.Context, id int, v []string, mode models.RelationshipUpdateMode) error { + switch mode { + case models.RelationshipUpdateModeSet: + return t.replaceJoins(ctx, id, v) + case models.RelationshipUpdateModeAdd: + return t.addJoins(ctx, id, v) + case models.RelationshipUpdateModeRemove: + return t.destroyJoins(ctx, id, v) + } + + return nil +} + type scenesMoviesTable struct { table } diff --git a/pkg/sqlite/tables.go b/pkg/sqlite/tables.go index 671a758feba..bcc82bcf59d 100644 --- a/pkg/sqlite/tables.go +++ b/pkg/sqlite/tables.go @@ -25,6 +25,7 @@ var ( scenesStashIDsJoinTable = goqu.T("scene_stash_ids") scenesMoviesJoinTable = goqu.T(moviesScenesTable) + performersAliasesJoinTable = goqu.T(performersAliasesTable) performersTagsJoinTable = goqu.T(performersTagsTable) performersStashIDsJoinTable = goqu.T("performer_stash_ids") ) @@ -184,6 +185,14 @@ var ( idColumn: goqu.T(performerTable).Col(idColumn), } + performersAliasesTableMgr = &stringTable{ + table: table{ + table: performersAliasesJoinTable, + idColumn: performersAliasesJoinTable.Col(performerIDColumn), + }, + stringColumn: performersAliasesJoinTable.Col(performerAliasColumn), + } + performersTagsTableMgr = &joinTable{ table: table{ table: performersTagsJoinTable, From d38280bd9fa595dc7a236344caeffd7cef69f2f6 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 3 Nov 2022 01:34:30 +0000 Subject: [PATCH 04/24] Fix unique indexes --- pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql b/pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql index 49672b84164..6c277a90a0e 100644 --- a/pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql +++ b/pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql @@ -13,4 +13,5 @@ DROP INDEX `performers_checksum_unique`; ALTER TABLE `performers` DROP COLUMN `checksum`; ALTER TABLE `performers` ADD COLUMN `disambiguation` varchar(255); -CREATE UNIQUE INDEX `performers_name_disambiguation` on `performers` (`name`, `disambiguation`); +CREATE UNIQUE INDEX `performers_name_disambiguation_unique` on `performers` (`name`, `disambiguation`) WHERE `disambiguation` IS NOT NULL; +CREATE UNIQUE INDEX `performers_name_unique` on `performers` (`name`) WHERE `disambiguation` IS NULL; From 136bd8579aa2da648b91b63c3f6bbafaaef038a9 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 3 Nov 2022 01:34:45 +0000 Subject: [PATCH 05/24] Fix post-migration --- pkg/sqlite/migrations/39_postmigrate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/sqlite/migrations/39_postmigrate.go b/pkg/sqlite/migrations/39_postmigrate.go index 671e365ef30..d82a4cf1721 100644 --- a/pkg/sqlite/migrations/39_postmigrate.go +++ b/pkg/sqlite/migrations/39_postmigrate.go @@ -66,7 +66,7 @@ func (m *schema39Migrator) migrate(ctx context.Context) error { aliases string ) - err := rows.Scan(id, aliases) + err := rows.Scan(&id, &aliases) if err != nil { return err } From ce2320f5f7f521529451e29cccdadff26519edbb Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 3 Nov 2022 02:41:40 +0000 Subject: [PATCH 06/24] Update UI for new fields --- graphql/documents/data/performer-slim.graphql | 1 + graphql/documents/data/performer.graphql | 3 +- graphql/schema/types/performer.graphql | 3 +- internal/api/resolver_mutation_performer.go | 5 ++ .../src/components/Galleries/GalleryCard.tsx | 12 ++--- .../Performers/EditPerformersDialog.tsx | 4 ++ .../components/Performers/PerformerCard.tsx | 11 +++- .../Performers/PerformerDetails/Performer.tsx | 11 ++-- .../PerformerDetails/PerformerEditPanel.tsx | 54 +++++++++++++++---- .../Performers/PerformerListTable.tsx | 2 +- ui/v2.5/src/components/Performers/styles.scss | 7 +++ ui/v2.5/src/components/Shared/GridCard.tsx | 2 +- .../src/components/Shared/TruncatedText.tsx | 2 +- ui/v2.5/src/locales/en-GB.json | 1 + 14 files changed, 91 insertions(+), 27 deletions(-) diff --git a/graphql/documents/data/performer-slim.graphql b/graphql/documents/data/performer-slim.graphql index 62d1b9b7aa2..0fc24b26829 100644 --- a/graphql/documents/data/performer-slim.graphql +++ b/graphql/documents/data/performer-slim.graphql @@ -1,6 +1,7 @@ fragment SlimPerformerData on Performer { id name + disambiguation gender url twitter diff --git a/graphql/documents/data/performer.graphql b/graphql/documents/data/performer.graphql index 2c27fb6ccc9..74d41349741 100644 --- a/graphql/documents/data/performer.graphql +++ b/graphql/documents/data/performer.graphql @@ -2,6 +2,7 @@ fragment PerformerData on Performer { id checksum name + disambiguation url gender twitter @@ -16,7 +17,7 @@ fragment PerformerData on Performer { career_length tattoos piercings - aliases + alias_list favorite ignore_auto_tag image_path diff --git a/graphql/schema/types/performer.graphql b/graphql/schema/types/performer.graphql index cfab68cb396..722b8f82525 100644 --- a/graphql/schema/types/performer.graphql +++ b/graphql/schema/types/performer.graphql @@ -28,7 +28,7 @@ type Performer { tattoos: String piercings: String aliases: String @deprecated(reason: "Use alias_list") - alias_list: [String!] + alias_list: [String!]! favorite: Boolean! tags: [Tag!]! ignore_auto_tag: Boolean! @@ -127,6 +127,7 @@ input BulkUpdateStrings { input BulkPerformerUpdateInput { clientMutationId: String ids: [ID!] + disambiguation: String url: String gender: GenderEnum birthdate: String diff --git a/internal/api/resolver_mutation_performer.go b/internal/api/resolver_mutation_performer.go index 02a2e77fcdb..4d70dbd4744 100644 --- a/internal/api/resolver_mutation_performer.go +++ b/internal/api/resolver_mutation_performer.go @@ -60,6 +60,9 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC CreatedAt: currentTime, UpdatedAt: currentTime, } + if input.Disambiguation != nil { + newPerformer.Disambiguation = *input.Disambiguation + } if input.URL != nil { newPerformer.URL = *input.URL } @@ -189,6 +192,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU } updatedPerformer.Name = translator.optionalString(input.Name, "name") + updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation") updatedPerformer.URL = translator.optionalString(input.URL, "url") if translator.hasField("gender") { @@ -314,6 +318,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe updatedPerformer := models.NewPerformerPartial() + updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation") updatedPerformer.URL = translator.optionalString(input.URL, "url") updatedPerformer.Birthdate = translator.optionalDate(input.Birthdate, "birthdate") updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity") diff --git a/ui/v2.5/src/components/Galleries/GalleryCard.tsx b/ui/v2.5/src/components/Galleries/GalleryCard.tsx index 1b7b81c5118..3c4215a11e4 100644 --- a/ui/v2.5/src/components/Galleries/GalleryCard.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryCard.tsx @@ -167,13 +167,11 @@ export const GalleryCard: React.FC = (props) => { details={
{props.gallery.date} -

- -

+
} popovers={maybeRenderPopoverButtonGroup()} diff --git a/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx b/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx index 634b055c20f..95d66e0a70e 100644 --- a/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx +++ b/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx @@ -29,6 +29,7 @@ interface IListOperationProps { const performerFields = [ "favorite", + "disambiguation", "url", "instagram", "twitter", @@ -243,6 +244,9 @@ export const EditPerformersDialog: React.FC = ( + {renderTextField("disambiguation", updateInput.disambiguation, (v) => + setUpdateField({ disambiguation: v }) + )} {renderTextField("birthdate", updateInput.birthdate, (v) => setUpdateField({ birthdate: v }) )} diff --git a/ui/v2.5/src/components/Performers/PerformerCard.tsx b/ui/v2.5/src/components/Performers/PerformerCard.tsx index 0f025998a64..350ac157d27 100644 --- a/ui/v2.5/src/components/Performers/PerformerCard.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCard.tsx @@ -204,7 +204,16 @@ export const PerformerCard: React.FC = ({ pretitleIcon={ } - title={performer.name ?? ""} + title={ +
+ {performer.name} + {performer.disambiguation && ( + + {` (${performer.disambiguation})`} + + )} +
+ } image={ <> = ({ performer }) => { } function maybeRenderAliases() { - if (performer?.aliases) { + if (performer?.alias_list?.length) { return (
{" "} - {performer.aliases} + {performer.alias_list?.join(", ")}
); } @@ -425,7 +425,12 @@ const PerformerPage: React.FC = ({ performer }) => { className="gender-icon mr-2 flag-icon" /> - {performer.name} + {performer.name} + {performer.disambiguation && ( + + {` (${performer.disambiguation})`} + + )} {renderClickableIcons()} = ({ const schema = yup.object({ name: yup.string().required(), - aliases: yup.string().optional(), + disambiguation: yup.string().optional(), + alias_list: yup + .array(yup.string().required()) + .optional() + .test({ + name: "unique", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + test: (value: any) => { + return (value ?? []).length === new Set(value).size; + }, + message: intl.formatMessage({ id: "dialogs.aliases_must_be_unique" }), + }), gender: yup.string().optional().oneOf(genderOptions), birthdate: yup.string().optional(), ethnicity: yup.string().optional(), @@ -127,7 +139,8 @@ export const PerformerEditPanel: React.FC = ({ const initialValues = { name: performer.name ?? "", - aliases: performer.aliases ?? "", + disambiguation: performer.disambiguation ?? "", + alias_list: performer.alias_list, gender: genderToString(performer.gender ?? undefined), birthdate: performer.birthdate ?? "", ethnicity: performer.ethnicity ?? "", @@ -262,9 +275,12 @@ export const PerformerEditPanel: React.FC = ({ if (state.name) { formik.setFieldValue("name", state.name); } - + // disambiguation if (state.aliases) { - formik.setFieldValue("aliases", state.aliases); + formik.setFieldValue( + "alias_list", + state.aliases.split(",").map((a) => a.trim()) + ); } if (state.birthdate) { formik.setFieldValue("birthdate", state.birthdate); @@ -856,16 +872,32 @@ export const PerformerEditPanel: React.FC = ({ - - - + + + - + + + {formik.errors.disambiguation} + + + + + + + + + + formik.setFieldValue("alias_list", value)} + errors={formik.errors.alias_list} /> diff --git a/ui/v2.5/src/components/Performers/PerformerListTable.tsx b/ui/v2.5/src/components/Performers/PerformerListTable.tsx index 74a5fabc18e..c8ef53a9449 100644 --- a/ui/v2.5/src/components/Performers/PerformerListTable.tsx +++ b/ui/v2.5/src/components/Performers/PerformerListTable.tsx @@ -67,7 +67,7 @@ export const PerformerListTable: React.FC = (
{performer.name}
- {performer.aliases ? performer.aliases : ""} + {performer.alias_list ? performer.alias_list.join(", ") : ""} {performer.favorite && ( ))} diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx index b9c7f4316ba..39d87c788a4 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx @@ -81,6 +81,7 @@ const PerformerStashBoxModal: React.FC = ({
  • ))} diff --git a/ui/v2.5/src/components/Performers/PerformerListTable.tsx b/ui/v2.5/src/components/Performers/PerformerListTable.tsx index c8ef53a9449..0b7d1be5709 100644 --- a/ui/v2.5/src/components/Performers/PerformerListTable.tsx +++ b/ui/v2.5/src/components/Performers/PerformerListTable.tsx @@ -64,7 +64,14 @@ export const PerformerListTable: React.FC = ( -
    {performer.name}
    +
    + {performer.name} + {performer.disambiguation && ( + + {` (${performer.disambiguation})`} + + )} +
    {performer.alias_list ? performer.alias_list.join(", ") : ""} diff --git a/ui/v2.5/src/components/Shared/Select.tsx b/ui/v2.5/src/components/Shared/Select.tsx index 4a88a17d12c..abb6032cfa2 100644 --- a/ui/v2.5/src/components/Shared/Select.tsx +++ b/ui/v2.5/src/components/Shared/Select.tsx @@ -91,6 +91,7 @@ interface ISelectProps { } interface IFilterComponentProps extends IFilterProps { items: Array; + toOption?: (item: ValidTypes) => Option; onCreate?: (name: string) => Promise<{ item: ValidTypes; message: string }>; } interface IFilterSelectProps @@ -268,10 +269,15 @@ const FilterSelectComponent = ( const selectedIds = ids ?? []; const Toast = useToast(); - const options = items.map((i) => ({ - value: i.id, - label: i.name ?? "", - })); + const options = items.map((i) => { + if (props.toOption) { + return props.toOption(i); + } + return { + value: i.id, + label: i.name ?? "", + }; + }); const selected = options.filter((option) => selectedIds.includes(option.value) @@ -473,6 +479,20 @@ export const PerformerSelect: React.FC = (props) => { const performers = data?.allPerformers ?? []; + type performerType = { + id: string; + name: string; + disambiguation?: string; + }; + + const toOption = (p: ValidTypes) => ({ + value: p.id, + label: `${p.name}${ + (p as performerType).disambiguation && + ` (${(p as performerType).disambiguation})` + }`, + }); + const onCreate = async (name: string) => { const result = await createPerformer({ variables: { input: { name } }, @@ -486,6 +506,7 @@ export const PerformerSelect: React.FC = (props) => { return ( = ({ [index: string]: unknown; } = { name: performer.name ?? "", - aliases: performer.aliases, + disambiguation: performer.disambiguation ?? "", + alias_list: + performer.aliases?.split(",").map((a) => a.trim()) ?? undefined, gender: stringToGender(performer.gender ?? undefined, true), birthdate: performer.birthdate, ethnicity: performer.ethnicity, @@ -200,6 +202,7 @@ const PerformerModal: React.FC = ({
    {renderField("name", performer.name)} + {renderField("disambiguation", performer.disambiguation)} {renderField("aliases", performer.aliases)} {renderField( "gender", diff --git a/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx b/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx index 2a1a1f4b7dc..0a38fab4fe8 100755 --- a/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx +++ b/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx @@ -368,7 +368,14 @@ const PerformerTaggerList: React.FC = ({ to={`/performers/${performer.id}`} className={`${CLASSNAME}-header`} > -

    {performer.name}

    +

    + {performer.name} + {performer.disambiguation && ( + + {` (${performer.disambiguation})`} + + )} +

    {mainContent}
    {subContent}
    diff --git a/ui/v2.5/src/components/Tagger/performers/StashSearchResult.tsx b/ui/v2.5/src/components/Tagger/performers/StashSearchResult.tsx index 88060cdad85..1779ebc423b 100755 --- a/ui/v2.5/src/components/Tagger/performers/StashSearchResult.tsx +++ b/ui/v2.5/src/components/Tagger/performers/StashSearchResult.tsx @@ -67,7 +67,10 @@ const StashSearchResult: React.FC = ({ onClick={() => setModalPerformer(p)} > - {p.name} + + {p.name} + {p.disambiguation && ` (${p.disambiguation})`} + )); From 239b802180f95150b42a05c9f28228e1ebe73feb Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 10 Nov 2022 14:48:08 +1100 Subject: [PATCH 08/24] Update schema number --- pkg/sqlite/database.go | 2 +- ...mbig_aliases.up.sql => 40_performer_disambig_aliases.up.sql} | 0 pkg/sqlite/migrations/{39_postmigrate.go => 40_postmigrate.go} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename pkg/sqlite/migrations/{39_performer_disambig_aliases.up.sql => 40_performer_disambig_aliases.up.sql} (100%) rename pkg/sqlite/migrations/{39_postmigrate.go => 40_postmigrate.go} (100%) diff --git a/pkg/sqlite/database.go b/pkg/sqlite/database.go index 11de785409d..550c6676398 100644 --- a/pkg/sqlite/database.go +++ b/pkg/sqlite/database.go @@ -22,7 +22,7 @@ import ( "github.com/stashapp/stash/pkg/logger" ) -var appSchemaVersion uint = 39 +var appSchemaVersion uint = 40 //go:embed migrations/*.sql var migrationsBox embed.FS diff --git a/pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql b/pkg/sqlite/migrations/40_performer_disambig_aliases.up.sql similarity index 100% rename from pkg/sqlite/migrations/39_performer_disambig_aliases.up.sql rename to pkg/sqlite/migrations/40_performer_disambig_aliases.up.sql diff --git a/pkg/sqlite/migrations/39_postmigrate.go b/pkg/sqlite/migrations/40_postmigrate.go similarity index 100% rename from pkg/sqlite/migrations/39_postmigrate.go rename to pkg/sqlite/migrations/40_postmigrate.go From edfb990a92f7c6b85e8cd3715a5563db0aac9a2f Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 10 Nov 2022 14:52:24 +1100 Subject: [PATCH 09/24] Fix postmigration --- pkg/sqlite/migrations/40_postmigrate.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/sqlite/migrations/40_postmigrate.go b/pkg/sqlite/migrations/40_postmigrate.go index d82a4cf1721..0f0be2d5b5b 100644 --- a/pkg/sqlite/migrations/40_postmigrate.go +++ b/pkg/sqlite/migrations/40_postmigrate.go @@ -11,14 +11,14 @@ import ( "github.com/stashapp/stash/pkg/sqlite" ) -type schema39Migrator struct { +type schema40Migrator struct { migrator } -func post39(ctx context.Context, db *sqlx.DB) error { +func post40(ctx context.Context, db *sqlx.DB) error { logger.Info("Running post-migration for schema version 39") - m := schema39Migrator{ + m := schema40Migrator{ migrator: migrator{ db: db, }, @@ -31,7 +31,7 @@ func post39(ctx context.Context, db *sqlx.DB) error { return nil } -func (m *schema39Migrator) migrate(ctx context.Context) error { +func (m *schema40Migrator) migrate(ctx context.Context) error { logger.Info("Migrating performer aliases") const ( @@ -102,7 +102,7 @@ func (m *schema39Migrator) migrate(ctx context.Context) error { return nil } -func (m *schema39Migrator) migratePerformerAliases(id int, aliases string) error { +func (m *schema40Migrator) migratePerformerAliases(id int, aliases string) error { // split aliases by comma aliasList := strings.Split(aliases, ",") @@ -126,5 +126,5 @@ func (m *schema39Migrator) migratePerformerAliases(id int, aliases string) error } func init() { - sqlite.RegisterPostMigration(39, post39) + sqlite.RegisterPostMigration(40, post40) } From f5acba5e1c5c76416602fe661915e399f4066068 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 10 Nov 2022 15:01:05 +1100 Subject: [PATCH 10/24] Add autotag support for performer aliases --- internal/autotag/gallery_test.go | 10 +- internal/autotag/image_test.go | 10 +- internal/autotag/integration_test.go | 9 ++ internal/autotag/performer.go | 135 ++++++++++++++++----------- internal/autotag/performer_test.go | 15 +-- internal/autotag/scene_test.go | 10 +- internal/manager/task_autotag.go | 10 +- pkg/match/path.go | 22 ++++- pkg/sqlite/performer.go | 8 +- 9 files changed, 150 insertions(+), 79 deletions(-) diff --git a/internal/autotag/gallery_test.go b/internal/autotag/gallery_test.go index cae45e6c963..556c09ce2e6 100644 --- a/internal/autotag/gallery_test.go +++ b/internal/autotag/gallery_test.go @@ -21,15 +21,17 @@ func TestGalleryPerformers(t *testing.T) { const performerName = "performer name" const performerID = 2 performer := models.Performer{ - ID: performerID, - Name: performerName, + ID: performerID, + Name: performerName, + Aliases: models.NewRelatedStrings([]string{}), } const reversedPerformerName = "name performer" const reversedPerformerID = 3 reversedPerformer := models.Performer{ - ID: reversedPerformerID, - Name: reversedPerformerName, + ID: reversedPerformerID, + Name: reversedPerformerName, + Aliases: models.NewRelatedStrings([]string{}), } testTables := generateTestTable(performerName, galleryExt) diff --git a/internal/autotag/image_test.go b/internal/autotag/image_test.go index b9884239869..62133aea8ce 100644 --- a/internal/autotag/image_test.go +++ b/internal/autotag/image_test.go @@ -18,15 +18,17 @@ func TestImagePerformers(t *testing.T) { const performerName = "performer name" const performerID = 2 performer := models.Performer{ - ID: performerID, - Name: performerName, + ID: performerID, + Name: performerName, + Aliases: models.NewRelatedStrings([]string{}), } const reversedPerformerName = "name performer" const reversedPerformerID = 3 reversedPerformer := models.Performer{ - ID: reversedPerformerID, - Name: reversedPerformerName, + ID: reversedPerformerID, + Name: reversedPerformerName, + Aliases: models.NewRelatedStrings([]string{}), } testTables := generateTestTable(performerName, imageExt) diff --git a/internal/autotag/integration_test.go b/internal/autotag/integration_test.go index 6ffd12263a9..07dd805e33f 100644 --- a/internal/autotag/integration_test.go +++ b/internal/autotag/integration_test.go @@ -539,6 +539,9 @@ func TestParsePerformerScenes(t *testing.T) { for _, p := range performers { if err := withTxn(func(ctx context.Context) error { + if err := p.LoadAliases(ctx, r.Performer); err != nil { + return err + } return PerformerScenes(ctx, p, nil, r.Scene, nil) }); err != nil { t.Errorf("Error auto-tagging performers: %s", err) @@ -694,6 +697,9 @@ func TestParsePerformerImages(t *testing.T) { for _, p := range performers { if err := withTxn(func(ctx context.Context) error { + if err := p.LoadAliases(ctx, r.Performer); err != nil { + return err + } return PerformerImages(ctx, p, nil, r.Image, nil) }); err != nil { t.Errorf("Error auto-tagging performers: %s", err) @@ -851,6 +857,9 @@ func TestParsePerformerGalleries(t *testing.T) { for _, p := range performers { if err := withTxn(func(ctx context.Context) error { + if err := p.LoadAliases(ctx, r.Performer); err != nil { + return err + } return PerformerGalleries(ctx, p, nil, r.Gallery, nil) }); err != nil { t.Errorf("Error auto-tagging performers: %s", err) diff --git a/internal/autotag/performer.go b/internal/autotag/performer.go index b879fe341fd..1f6e535125c 100644 --- a/internal/autotag/performer.go +++ b/internal/autotag/performer.go @@ -29,77 +29,106 @@ type GalleryQueryPerformerUpdater interface { gallery.PartialUpdater } -func getPerformerTagger(p *models.Performer, cache *match.Cache) tagger { - return tagger{ +func getPerformerTaggers(p *models.Performer, cache *match.Cache) []tagger { + ret := []tagger{{ ID: p.ID, Type: "performer", Name: p.Name, cache: cache, + }} + + for _, a := range p.Aliases.List() { + ret = append(ret, tagger{ + ID: p.ID, + Type: "performer", + Name: a, + cache: cache, + }) } + + return ret } // PerformerScenes searches for scenes whose path matches the provided performer name and tags the scene with the performer. +// Performer aliases must be loaded. func PerformerScenes(ctx context.Context, p *models.Performer, paths []string, rw SceneQueryPerformerUpdater, cache *match.Cache) error { - t := getPerformerTagger(p, cache) - - return t.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) { - if err := o.LoadPerformerIDs(ctx, rw); err != nil { - return false, err - } - existing := o.PerformerIDs.List() - - if intslice.IntInclude(existing, p.ID) { - return false, nil - } - - if err := scene.AddPerformer(ctx, rw, o, p.ID); err != nil { - return false, err + t := getPerformerTaggers(p, cache) + + for _, tt := range t { + if err := tt.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) { + if err := o.LoadPerformerIDs(ctx, rw); err != nil { + return false, err + } + existing := o.PerformerIDs.List() + + if intslice.IntInclude(existing, p.ID) { + return false, nil + } + + if err := scene.AddPerformer(ctx, rw, o, p.ID); err != nil { + return false, err + } + + return true, nil + }); err != nil { + return err } - - return true, nil - }) + } + return nil } // PerformerImages searches for images whose path matches the provided performer name and tags the image with the performer. +// Performer aliases must be loaded. func PerformerImages(ctx context.Context, p *models.Performer, paths []string, rw ImageQueryPerformerUpdater, cache *match.Cache) error { - t := getPerformerTagger(p, cache) - - return t.tagImages(ctx, paths, rw, func(o *models.Image) (bool, error) { - if err := o.LoadPerformerIDs(ctx, rw); err != nil { - return false, err - } - existing := o.PerformerIDs.List() - - if intslice.IntInclude(existing, p.ID) { - return false, nil + t := getPerformerTaggers(p, cache) + + for _, tt := range t { + if err := tt.tagImages(ctx, paths, rw, func(o *models.Image) (bool, error) { + if err := o.LoadPerformerIDs(ctx, rw); err != nil { + return false, err + } + existing := o.PerformerIDs.List() + + if intslice.IntInclude(existing, p.ID) { + return false, nil + } + + if err := image.AddPerformer(ctx, rw, o, p.ID); err != nil { + return false, err + } + + return true, nil + }); err != nil { + return err } - - if err := image.AddPerformer(ctx, rw, o, p.ID); err != nil { - return false, err - } - - return true, nil - }) + } + return nil } // PerformerGalleries searches for galleries whose path matches the provided performer name and tags the gallery with the performer. +// Performer aliases must be loaded. func PerformerGalleries(ctx context.Context, p *models.Performer, paths []string, rw GalleryQueryPerformerUpdater, cache *match.Cache) error { - t := getPerformerTagger(p, cache) - - return t.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) { - if err := o.LoadPerformerIDs(ctx, rw); err != nil { - return false, err + t := getPerformerTaggers(p, cache) + + for _, tt := range t { + if err := tt.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) { + if err := o.LoadPerformerIDs(ctx, rw); err != nil { + return false, err + } + existing := o.PerformerIDs.List() + + if intslice.IntInclude(existing, p.ID) { + return false, nil + } + + if err := gallery.AddPerformer(ctx, rw, o, p.ID); err != nil { + return false, err + } + + return true, nil + }); err != nil { + return err } - existing := o.PerformerIDs.List() - - if intslice.IntInclude(existing, p.ID) { - return false, nil - } - - if err := gallery.AddPerformer(ctx, rw, o, p.ID); err != nil { - return false, err - } - - return true, nil - }) + } + return nil } diff --git a/internal/autotag/performer_test.go b/internal/autotag/performer_test.go index 422e406451e..954a226e377 100644 --- a/internal/autotag/performer_test.go +++ b/internal/autotag/performer_test.go @@ -59,8 +59,9 @@ func testPerformerScenes(t *testing.T, performerName, expectedRegex string) { } performer := models.Performer{ - ID: performerID, - Name: performerName, + ID: performerID, + Name: performerName, + Aliases: models.NewRelatedStrings([]string{}), } organized := false @@ -139,8 +140,9 @@ func testPerformerImages(t *testing.T, performerName, expectedRegex string) { } performer := models.Performer{ - ID: performerID, - Name: performerName, + ID: performerID, + Name: performerName, + Aliases: models.NewRelatedStrings([]string{}), } organized := false @@ -220,8 +222,9 @@ func testPerformerGalleries(t *testing.T, performerName, expectedRegex string) { } performer := models.Performer{ - ID: performerID, - Name: performerName, + ID: performerID, + Name: performerName, + Aliases: models.NewRelatedStrings([]string{}), } organized := false diff --git a/internal/autotag/scene_test.go b/internal/autotag/scene_test.go index cb0ff32db76..71a28336c58 100644 --- a/internal/autotag/scene_test.go +++ b/internal/autotag/scene_test.go @@ -151,15 +151,17 @@ func TestScenePerformers(t *testing.T) { const performerName = "performer name" const performerID = 2 performer := models.Performer{ - ID: performerID, - Name: performerName, + ID: performerID, + Name: performerName, + Aliases: models.NewRelatedStrings([]string{}), } const reversedPerformerName = "name performer" const reversedPerformerID = 3 reversedPerformer := models.Performer{ - ID: reversedPerformerID, - Name: reversedPerformerName, + ID: reversedPerformerID, + Name: reversedPerformerName, + Aliases: models.NewRelatedStrings([]string{}), } testTables := generateTestTable(performerName, sceneExt) diff --git a/internal/manager/task_autotag.go b/internal/manager/task_autotag.go index ee487b72406..550a28089c0 100644 --- a/internal/manager/task_autotag.go +++ b/internal/manager/task_autotag.go @@ -137,22 +137,26 @@ func (j *autoTagJob) autoTagPerformers(ctx context.Context, progress *job.Progre PerPage: &perPage, }) if err != nil { - return fmt.Errorf("error querying performers: %v", err) + return fmt.Errorf("error querying performers: %w", err) } } else { performerIdInt, err := strconv.Atoi(performerId) if err != nil { - return fmt.Errorf("error parsing performer id %s: %s", performerId, err.Error()) + return fmt.Errorf("parsing performer id %s: %w", performerId, err) } performer, err := performerQuery.Find(ctx, performerIdInt) if err != nil { - return fmt.Errorf("error finding performer id %s: %s", performerId, err.Error()) + return fmt.Errorf("finding performer id %s: %w", performerId, err) } if performer == nil { return fmt.Errorf("performer with id %s not found", performerId) } + + if err := performer.LoadAliases(ctx, j.txnManager.Performer); err != nil { + return fmt.Errorf("loading aliases for performer %d: %w", performer.ID, err) + } performers = append(performers, performer) } diff --git a/pkg/match/path.go b/pkg/match/path.go index b150809a3e0..157ccbc2e27 100644 --- a/pkg/match/path.go +++ b/pkg/match/path.go @@ -30,6 +30,7 @@ var separatorRE = regexp.MustCompile(separatorPattern) type PerformerAutoTagQueryer interface { Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) QueryForAutoTag(ctx context.Context, words []string) ([]*models.Performer, error) + models.AliasLoader } type StudioAutoTagQueryer interface { @@ -168,8 +169,25 @@ func PathToPerformers(ctx context.Context, path string, reader PerformerAutoTagQ var ret []*models.Performer for _, p := range performers { - // TODO - commenting out alias handling until both sides work correctly - if nameMatchesPath(p.Name, path) != -1 { // || nameMatchesPath(p.Aliases.String, path) { + matches := false + if nameMatchesPath(p.Name, path) != -1 { + matches = true + } + + if !matches { + if err := p.LoadAliases(ctx, reader); err != nil { + return nil, err + } + + for _, alias := range p.Aliases.List() { + if nameMatchesPath(alias, path) != -1 { + matches = true + break + } + } + } + + if matches { ret = append(ret, p) } } diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index dde8c535bb5..99a07b93c85 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -448,14 +448,16 @@ func (qb *PerformerStore) QueryForAutoTag(ctx context.Context, words []string) ( // TODO - Query needs to be changed to support queries of this type, and // this method should be removed table := qb.table() - sq := dialect.From(table).Select(table.Col(idColumn)).Where() + sq := dialect.From(table).Select(table.Col(idColumn)).LeftJoin( + performersAliasesJoinTable, + goqu.On(performersAliasesJoinTable.Col(performerIDColumn).Eq(table.Col(idColumn))), + ) var whereClauses []exp.Expression for _, w := range words { whereClauses = append(whereClauses, table.Col("name").Like(w+"%")) - // TODO - commented out until alias matching works both ways - // whereClauses = append(whereClauses, table.Col("aliases").Like(w+"%") + whereClauses = append(whereClauses, performersAliasesJoinTable.Col("alias").Like(w+"%")) } sq = sq.Where( From 5c3d9119406d571a6be6b37be743c64b4f1d8be5 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 10 Nov 2022 16:08:45 +1100 Subject: [PATCH 11/24] Add disambiguation filter criterion --- graphql/schema/types/filters.graphql | 1 + pkg/models/performer.go | 11 ++++++----- pkg/sqlite/performer.go | 1 + 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index f60f37a69cd..86174bb5011 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -45,6 +45,7 @@ input PerformerFilterType { NOT: PerformerFilterType name: StringCriterionInput + disambiguation: StringCriterionInput details: StringCriterionInput """Filter by favorite""" diff --git a/pkg/models/performer.go b/pkg/models/performer.go index e72a19b6e06..7f5b7e3cdd4 100644 --- a/pkg/models/performer.go +++ b/pkg/models/performer.go @@ -62,11 +62,12 @@ type GenderCriterionInput struct { } type PerformerFilterType struct { - And *PerformerFilterType `json:"AND"` - Or *PerformerFilterType `json:"OR"` - Not *PerformerFilterType `json:"NOT"` - Name *StringCriterionInput `json:"name"` - Details *StringCriterionInput `json:"details"` + And *PerformerFilterType `json:"AND"` + Or *PerformerFilterType `json:"OR"` + Not *PerformerFilterType `json:"NOT"` + Name *StringCriterionInput `json:"name"` + Disambiguation *StringCriterionInput `json:"disambiguation"` + Details *StringCriterionInput `json:"details"` // Filter by favorite FilterFavorites *bool `json:"filter_favorites"` // Filter by birth year diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index 99a07b93c85..390238a4b9e 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -536,6 +536,7 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform const tableName = performerTable query.handleCriterion(ctx, stringCriterionHandler(filter.Name, tableName+".name")) + query.handleCriterion(ctx, stringCriterionHandler(filter.Disambiguation, tableName+".disambiguation")) query.handleCriterion(ctx, stringCriterionHandler(filter.Details, tableName+".details")) query.handleCriterion(ctx, boolCriterionHandler(filter.FilterFavorites, tableName+".favorite", nil)) From 91f8d8e2aea2fd9b14bf0d9d2a39744952066c00 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 10 Nov 2022 16:09:09 +1100 Subject: [PATCH 12/24] Improve name matching during import --- pkg/models/jsonschema/performer.go | 25 +++--- pkg/performer/export.go | 39 ++++----- pkg/performer/export_test.go | 123 +++++++++++++++-------------- pkg/performer/import.go | 64 +++++++++------ pkg/performer/import_test.go | 22 +++++- pkg/performer/update.go | 1 + 6 files changed, 159 insertions(+), 115 deletions(-) diff --git a/pkg/models/jsonschema/performer.go b/pkg/models/jsonschema/performer.go index 0a98ff9a9fc..c0996a1a580 100644 --- a/pkg/models/jsonschema/performer.go +++ b/pkg/models/jsonschema/performer.go @@ -34,15 +34,16 @@ func (s *StringOrStringList) UnmarshalJSON(data []byte) error { } type Performer struct { - Name string `json:"name,omitempty"` - Gender string `json:"gender,omitempty"` - URL string `json:"url,omitempty"` - Twitter string `json:"twitter,omitempty"` - Instagram string `json:"instagram,omitempty"` - Birthdate string `json:"birthdate,omitempty"` - Ethnicity string `json:"ethnicity,omitempty"` - Country string `json:"country,omitempty"` - EyeColor string `json:"eye_color,omitempty"` + Name string `json:"name,omitempty"` + Disambiguation string `json:"disambiguation,omitempty"` + Gender string `json:"gender,omitempty"` + URL string `json:"url,omitempty"` + Twitter string `json:"twitter,omitempty"` + Instagram string `json:"instagram,omitempty"` + Birthdate string `json:"birthdate,omitempty"` + Ethnicity string `json:"ethnicity,omitempty"` + Country string `json:"country,omitempty"` + EyeColor string `json:"eye_color,omitempty"` // this should be int, but keeping string for backwards compatibility Height string `json:"height,omitempty"` Measurements string `json:"measurements,omitempty"` @@ -66,7 +67,11 @@ type Performer struct { } func (s Performer) Filename() string { - return fsutil.SanitiseBasename(s.Name) + ".json" + name := s.Name + if s.Disambiguation != "" { + name += "_" + s.Disambiguation + } + return fsutil.SanitiseBasename(name) + ".json" } func LoadPerformerFile(filePath string) (*Performer, error) { diff --git a/pkg/performer/export.go b/pkg/performer/export.go index e88a77034b1..2d87d0df633 100644 --- a/pkg/performer/export.go +++ b/pkg/performer/export.go @@ -20,25 +20,26 @@ type ImageAliasStashIDGetter interface { // ToJSON converts a Performer object into its JSON equivalent. func ToJSON(ctx context.Context, reader ImageAliasStashIDGetter, performer *models.Performer) (*jsonschema.Performer, error) { newPerformerJSON := jsonschema.Performer{ - Name: performer.Name, - Gender: performer.Gender.String(), - URL: performer.URL, - Ethnicity: performer.Ethnicity, - Country: performer.Country, - EyeColor: performer.EyeColor, - Measurements: performer.Measurements, - FakeTits: performer.FakeTits, - CareerLength: performer.CareerLength, - Tattoos: performer.Tattoos, - Piercings: performer.Piercings, - Twitter: performer.Twitter, - Instagram: performer.Instagram, - Favorite: performer.Favorite, - Details: performer.Details, - HairColor: performer.HairColor, - IgnoreAutoTag: performer.IgnoreAutoTag, - CreatedAt: json.JSONTime{Time: performer.CreatedAt}, - UpdatedAt: json.JSONTime{Time: performer.UpdatedAt}, + Name: performer.Name, + Disambiguation: performer.Disambiguation, + Gender: performer.Gender.String(), + URL: performer.URL, + Ethnicity: performer.Ethnicity, + Country: performer.Country, + EyeColor: performer.EyeColor, + Measurements: performer.Measurements, + FakeTits: performer.FakeTits, + CareerLength: performer.CareerLength, + Tattoos: performer.Tattoos, + Piercings: performer.Piercings, + Twitter: performer.Twitter, + Instagram: performer.Instagram, + Favorite: performer.Favorite, + Details: performer.Details, + HairColor: performer.HairColor, + IgnoreAutoTag: performer.IgnoreAutoTag, + CreatedAt: json.JSONTime{Time: performer.CreatedAt}, + UpdatedAt: json.JSONTime{Time: performer.UpdatedAt}, } if performer.Birthdate != nil { diff --git a/pkg/performer/export_test.go b/pkg/performer/export_test.go index a5b990bf330..83278b5eb53 100644 --- a/pkg/performer/export_test.go +++ b/pkg/performer/export_test.go @@ -21,21 +21,22 @@ const ( ) const ( - performerName = "testPerformer" - url = "url" - careerLength = "careerLength" - country = "country" - ethnicity = "ethnicity" - eyeColor = "eyeColor" - fakeTits = "fakeTits" - gender = "gender" - instagram = "instagram" - measurements = "measurements" - piercings = "piercings" - tattoos = "tattoos" - twitter = "twitter" - details = "details" - hairColor = "hairColor" + performerName = "testPerformer" + disambiguation = "disambiguation" + url = "url" + careerLength = "careerLength" + country = "country" + ethnicity = "ethnicity" + eyeColor = "eyeColor" + fakeTits = "fakeTits" + gender = "gender" + instagram = "instagram" + measurements = "measurements" + piercings = "piercings" + tattoos = "tattoos" + twitter = "twitter" + details = "details" + hairColor = "hairColor" autoTagIgnored = true ) @@ -69,34 +70,35 @@ var ( func createFullPerformer(id int, name string) *models.Performer { return &models.Performer{ - ID: id, - Name: name, - URL: url, - Aliases: models.NewRelatedStrings(aliases), - Birthdate: &birthDate, - CareerLength: careerLength, - Country: country, - Ethnicity: ethnicity, - EyeColor: eyeColor, - FakeTits: fakeTits, - Favorite: true, - Gender: gender, - Height: &height, - Instagram: instagram, - Measurements: measurements, - Piercings: piercings, - Tattoos: tattoos, - Twitter: twitter, - CreatedAt: createTime, - UpdatedAt: updateTime, - Rating: &rating, - Details: details, - DeathDate: &deathDate, - HairColor: hairColor, - Weight: &weight, - IgnoreAutoTag: autoTagIgnored, - TagIDs: models.NewRelatedIDs([]int{}), - StashIDs: models.NewRelatedStashIDs(stashIDs), + ID: id, + Name: name, + Disambiguation: disambiguation, + URL: url, + Aliases: models.NewRelatedStrings(aliases), + Birthdate: &birthDate, + CareerLength: careerLength, + Country: country, + Ethnicity: ethnicity, + EyeColor: eyeColor, + FakeTits: fakeTits, + Favorite: true, + Gender: gender, + Height: &height, + Instagram: instagram, + Measurements: measurements, + Piercings: piercings, + Tattoos: tattoos, + Twitter: twitter, + CreatedAt: createTime, + UpdatedAt: updateTime, + Rating: &rating, + Details: details, + DeathDate: &deathDate, + HairColor: hairColor, + Weight: &weight, + IgnoreAutoTag: autoTagIgnored, + TagIDs: models.NewRelatedIDs([]int{}), + StashIDs: models.NewRelatedStashIDs(stashIDs), } } @@ -113,23 +115,24 @@ func createEmptyPerformer(id int) models.Performer { func createFullJSONPerformer(name string, image string) *jsonschema.Performer { return &jsonschema.Performer{ - Name: name, - URL: url, - Aliases: aliases, - Birthdate: birthDate.String(), - CareerLength: careerLength, - Country: country, - Ethnicity: ethnicity, - EyeColor: eyeColor, - FakeTits: fakeTits, - Favorite: true, - Gender: gender, - Height: strconv.Itoa(height), - Instagram: instagram, - Measurements: measurements, - Piercings: piercings, - Tattoos: tattoos, - Twitter: twitter, + Name: name, + Disambiguation: disambiguation, + URL: url, + Aliases: aliases, + Birthdate: birthDate.String(), + CareerLength: careerLength, + Country: country, + Ethnicity: ethnicity, + EyeColor: eyeColor, + FakeTits: fakeTits, + Favorite: true, + Gender: gender, + Height: strconv.Itoa(height), + Instagram: instagram, + Measurements: measurements, + Piercings: piercings, + Tattoos: tattoos, + Twitter: twitter, CreatedAt: json.JSONTime{ Time: createTime, }, diff --git a/pkg/performer/import.go b/pkg/performer/import.go index 0f202a607c3..beebab35d52 100644 --- a/pkg/performer/import.go +++ b/pkg/performer/import.go @@ -131,8 +131,27 @@ func (i *Importer) Name() string { } func (i *Importer) FindExistingID(ctx context.Context) (*int, error) { - const nocase = false - existing, err := i.ReaderWriter.FindByNames(ctx, []string{i.Name()}, nocase) + // use disambiguation as well + performerFilter := models.PerformerFilterType{ + Name: &models.StringCriterionInput{ + Value: i.Input.Name, + Modifier: models.CriterionModifierEquals, + }, + } + + if i.Input.Disambiguation != "" { + performerFilter.Disambiguation = &models.StringCriterionInput{ + Value: i.Input.Disambiguation, + Modifier: models.CriterionModifierEquals, + } + } + + pp := 1 + findFilter := models.FindFilterType{ + PerPage: &pp, + } + + existing, _, err := i.ReaderWriter.Query(ctx, &performerFilter, &findFilter) if err != nil { return nil, err } @@ -168,26 +187,27 @@ func (i *Importer) Update(ctx context.Context, id int) error { func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Performer { newPerformer := models.Performer{ - Name: performerJSON.Name, - Gender: models.GenderEnum(performerJSON.Gender), - URL: performerJSON.URL, - Ethnicity: performerJSON.Ethnicity, - Country: performerJSON.Country, - EyeColor: performerJSON.EyeColor, - Measurements: performerJSON.Measurements, - FakeTits: performerJSON.FakeTits, - CareerLength: performerJSON.CareerLength, - Tattoos: performerJSON.Tattoos, - Piercings: performerJSON.Piercings, - Aliases: models.NewRelatedStrings(performerJSON.Aliases), - Twitter: performerJSON.Twitter, - Instagram: performerJSON.Instagram, - Details: performerJSON.Details, - HairColor: performerJSON.HairColor, - Favorite: performerJSON.Favorite, - IgnoreAutoTag: performerJSON.IgnoreAutoTag, - CreatedAt: performerJSON.CreatedAt.GetTime(), - UpdatedAt: performerJSON.UpdatedAt.GetTime(), + Name: performerJSON.Name, + Disambiguation: performerJSON.Disambiguation, + Gender: models.GenderEnum(performerJSON.Gender), + URL: performerJSON.URL, + Ethnicity: performerJSON.Ethnicity, + Country: performerJSON.Country, + EyeColor: performerJSON.EyeColor, + Measurements: performerJSON.Measurements, + FakeTits: performerJSON.FakeTits, + CareerLength: performerJSON.CareerLength, + Tattoos: performerJSON.Tattoos, + Piercings: performerJSON.Piercings, + Aliases: models.NewRelatedStrings(performerJSON.Aliases), + Twitter: performerJSON.Twitter, + Instagram: performerJSON.Instagram, + Details: performerJSON.Details, + HairColor: performerJSON.HairColor, + Favorite: performerJSON.Favorite, + IgnoreAutoTag: performerJSON.IgnoreAutoTag, + CreatedAt: performerJSON.CreatedAt.GetTime(), + UpdatedAt: performerJSON.UpdatedAt.GetTime(), TagIDs: models.NewRelatedIDs([]int{}), StashIDs: models.NewRelatedStashIDs(performerJSON.StashIDs), diff --git a/pkg/performer/import_test.go b/pkg/performer/import_test.go index cc2170f40aa..5cfd9c90d1c 100644 --- a/pkg/performer/import_test.go +++ b/pkg/performer/import_test.go @@ -179,14 +179,28 @@ func TestImporterFindExistingID(t *testing.T) { }, } + pp := 1 + findFilter := &models.FindFilterType{ + PerPage: &pp, + } + + performerFilter := func(name string) *models.PerformerFilterType { + return &models.PerformerFilterType{ + Name: &models.StringCriterionInput{ + Value: name, + Modifier: models.CriterionModifierEquals, + }, + } + } + errFindByNames := errors.New("FindByNames error") - readerWriter.On("FindByNames", testCtx, []string{performerName}, false).Return(nil, nil).Once() - readerWriter.On("FindByNames", testCtx, []string{existingPerformerName}, false).Return([]*models.Performer{ + readerWriter.On("Query", testCtx, performerFilter(performerName), findFilter).Return(nil, 0, nil).Once() + readerWriter.On("Query", testCtx, performerFilter(existingPerformerName), findFilter).Return([]*models.Performer{ { ID: existingPerformerID, }, - }, nil).Once() - readerWriter.On("FindByNames", testCtx, []string{performerNameErr}, false).Return(nil, errFindByNames).Once() + }, 1, nil).Once() + readerWriter.On("Query", testCtx, performerFilter(performerNameErr), findFilter).Return(nil, 0, errFindByNames).Once() id, err := i.FindExistingID(testCtx) assert.Nil(t, id) diff --git a/pkg/performer/update.go b/pkg/performer/update.go index ed10246fa40..d846eb6ce93 100644 --- a/pkg/performer/update.go +++ b/pkg/performer/update.go @@ -8,5 +8,6 @@ import ( type NameFinderCreator interface { FindByNames(ctx context.Context, names []string, nocase bool) ([]*models.Performer, error) + Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) Create(ctx context.Context, newPerformer *models.Performer) error } From e651cd30e414b08d4da3a565bc0ba0c189c3d930 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 10 Nov 2022 16:15:13 +1100 Subject: [PATCH 13/24] Add disambiguation filtering in UI --- ui/v2.5/src/models/list-filter/criteria/factory.ts | 1 + ui/v2.5/src/models/list-filter/performers.ts | 1 + ui/v2.5/src/models/list-filter/types.ts | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/v2.5/src/models/list-filter/criteria/factory.ts b/ui/v2.5/src/models/list-filter/criteria/factory.ts index e3738468399..c5f3c0b65d2 100644 --- a/ui/v2.5/src/models/list-filter/criteria/factory.ts +++ b/ui/v2.5/src/models/list-filter/criteria/factory.ts @@ -168,6 +168,7 @@ export function makeCriteria(type: CriterionType = "none") { case "director": case "synopsis": case "description": + case "disambiguation": return new StringCriterion(new StringCriterionOption(type, type)); case "scene_code": return new StringCriterion(new StringCriterionOption(type, type, "code")); diff --git a/ui/v2.5/src/models/list-filter/performers.ts b/ui/v2.5/src/models/list-filter/performers.ts index c2836544ad0..0471b233434 100644 --- a/ui/v2.5/src/models/list-filter/performers.ts +++ b/ui/v2.5/src/models/list-filter/performers.ts @@ -54,6 +54,7 @@ const numberCriteria: CriterionType[] = [ const stringCriteria: CriterionType[] = [ "name", + "disambiguation", "details", "ethnicity", "country", diff --git a/ui/v2.5/src/models/list-filter/types.ts b/ui/v2.5/src/models/list-filter/types.ts index 89dec60b16a..7ccf5932963 100644 --- a/ui/v2.5/src/models/list-filter/types.ts +++ b/ui/v2.5/src/models/list-filter/types.ts @@ -127,4 +127,5 @@ export type CriterionType = | "ignore_auto_tag" | "file_count" | "description" - | "scene_code"; + | "scene_code" + | "disambiguation"; From e7b7e373ed6e6f7638ac316e721ac19e3e37d75b Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 10 Nov 2022 16:51:28 +1100 Subject: [PATCH 14/24] Include aliases in performer select --- graphql/documents/data/performer-slim.graphql | 1 + ui/v2.5/src/components/Shared/Select.tsx | 117 ++++++++++++++++-- 2 files changed, 105 insertions(+), 13 deletions(-) diff --git a/graphql/documents/data/performer-slim.graphql b/graphql/documents/data/performer-slim.graphql index 0fc24b26829..dc97a33496d 100644 --- a/graphql/documents/data/performer-slim.graphql +++ b/graphql/documents/data/performer-slim.graphql @@ -19,6 +19,7 @@ fragment SlimPerformerData on Performer { career_length tattoos piercings + alias_list tags { id name diff --git a/ui/v2.5/src/components/Shared/Select.tsx b/ui/v2.5/src/components/Shared/Select.tsx index abb6032cfa2..0a9d1c3e22f 100644 --- a/ui/v2.5/src/components/Shared/Select.tsx +++ b/ui/v2.5/src/components/Shared/Select.tsx @@ -469,6 +469,13 @@ export const MarkerTitleSuggest: React.FC = (props) => { }; export const PerformerSelect: React.FC = (props) => { + const [performerAliases, setPerformerAliases] = useState< + Record + >({}); + const [performerDisambiguations, setPerformerDisambiguations] = useState< + Record + >({}); + const [allAliases, setAllAliases] = useState([]); const { data, loading } = useAllPerformersForFilter(); const [createPerformer] = usePerformerCreate(); @@ -477,21 +484,103 @@ export const PerformerSelect: React.FC = (props) => { const defaultCreatable = !configuration?.interface.disableDropdownCreate.performer ?? true; - const performers = data?.allPerformers ?? []; + const performers = useMemo(() => data?.allPerformers ?? [], [ + data?.allPerformers, + ]); + + useEffect(() => { + // build the tag aliases map + const newAliases: Record = {}; + const newDisambiguations: Record = {}; + const newAll: string[] = []; + performers.forEach((t) => { + if (t.alias_list.length) { + newAliases[t.id] = t.alias_list; + } + newAll.push(...t.alias_list); + if (t.disambiguation) { + newDisambiguations[t.id] = t.disambiguation; + } + }); + setPerformerAliases(newAliases); + setAllAliases(newAll); + setPerformerDisambiguations(newDisambiguations); + }, [performers]); + + const PerformerOption: React.FC> = ( + optionProps + ) => { + const { inputValue } = optionProps.selectProps; + + let thisOptionProps = optionProps; + + let { label } = optionProps.data; + const id = Number(optionProps.data.value); + + if (id && performerDisambiguations[id]) { + label += ` (${performerDisambiguations[id]})`; + } - type performerType = { - id: string; - name: string; - disambiguation?: string; + if ( + inputValue && + !optionProps.label.toLowerCase().includes(inputValue.toLowerCase()) + ) { + // must be alias + label += " (alias)"; + } + + if (label != optionProps.data.label) { + thisOptionProps = { + ...optionProps, + children: label, + }; + } + + return ; }; - const toOption = (p: ValidTypes) => ({ - value: p.id, - label: `${p.name}${ - (p as performerType).disambiguation && - ` (${(p as performerType).disambiguation})` - }`, - }); + const filterOption = (option: Option, rawInput: string): boolean => { + if (!rawInput) { + return true; + } + + const input = rawInput.toLowerCase(); + const optionVal = option.label.toLowerCase(); + + if (optionVal.includes(input)) { + return true; + } + + // search for performer aliases + const aliases = performerAliases[option.value]; + return aliases && aliases.some((a) => a.toLowerCase().includes(input)); + }; + + const isValidNewOption = ( + inputValue: string, + value: ValueType, + options: OptionsType