diff --git a/graphql/schema/types/scraper.graphql b/graphql/schema/types/scraper.graphql index 2c13872f3..cb193f47d 100644 --- a/graphql/schema/types/scraper.graphql +++ b/graphql/schema/types/scraper.graphql @@ -281,7 +281,10 @@ type StashBoxFingerprint { duration: Int! } -"If neither ids nor names are set, tag all items" +""" +Accepts either ids, or a combination of names and stash_ids. +If none are set, then all existing items will be tagged. +""" input StashBoxBatchTagInput { "Stash endpoint to use for the tagging" endpoint: Int @deprecated(reason: "use stash_box_endpoint") @@ -293,12 +296,17 @@ input StashBoxBatchTagInput { refresh: Boolean! "If batch adding studios, should their parent studios also be created?" createParent: Boolean! - "If set, only tag these ids" + """ + IDs in stash of the items to update. + If set, names and stash_ids fields will be ignored. + """ ids: [ID!] - "If set, only tag these names" + "Names of the items in the stash-box instance to search for and create" names: [String!] - "If set, only tag these performer ids" + "Stash IDs of the items in the stash-box instance to search for and create" + stash_ids: [String!] + "IDs in stash of the performers to update" performer_ids: [ID!] @deprecated(reason: "use ids") - "If set, only tag these performer names" + "Names of the performers in the stash-box instance to search for and create" performer_names: [String!] @deprecated(reason: "use names") } diff --git a/internal/api/resolver_mutation_stash_box.go b/internal/api/resolver_mutation_stash_box.go index 4026667eb..436937511 100644 --- a/internal/api/resolver_mutation_stash_box.go +++ b/internal/api/resolver_mutation_stash_box.go @@ -39,7 +39,7 @@ func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input } func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input manager.StashBoxBatchTagInput) (string, error) { - b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint) + b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint) //nolint:staticcheck if err != nil { return "", err } @@ -49,7 +49,7 @@ func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input } func (r *mutationResolver) StashBoxBatchStudioTag(ctx context.Context, input manager.StashBoxBatchTagInput) (string, error) { - b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint) + b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint) //nolint:staticcheck if err != nil { return "", err } diff --git a/internal/manager/manager_tasks.go b/internal/manager/manager_tasks.go index 085c4459e..1e66433be 100644 --- a/internal/manager/manager_tasks.go +++ b/internal/manager/manager_tasks.go @@ -365,9 +365,37 @@ func (s *Manager) MigrateHash(ctx context.Context) int { return s.JobManager.Add(ctx, "Migrating scene hashes...", j) } -// If neither ids nor names are set, tag all items +// batchTagType indicates which batch tagging mode to use +type batchTagType int + +const ( + batchTagByIds batchTagType = iota + batchTagByNamesOrStashIds + batchTagAll +) + +// getBatchTagType determines the batch tag mode based on the input +func (input StashBoxBatchTagInput) getBatchTagType(hasPerformerFields bool) batchTagType { + switch { + case len(input.Ids) > 0: + return batchTagByIds + case hasPerformerFields && len(input.PerformerIds) > 0: + return batchTagByIds + case len(input.StashIDs) > 0 || len(input.Names) > 0: + return batchTagByNamesOrStashIds + case hasPerformerFields && len(input.PerformerNames) > 0: + return batchTagByNamesOrStashIds + default: + return batchTagAll + } +} + +// Accepts either ids, or a combination of names and stash_ids. +// If none are set, then all existing items will be tagged. type StashBoxBatchTagInput struct { - // Stash endpoint to use for the tagging - deprecated - use StashBoxEndpoint + // Stash endpoint to use for the tagging + // + // Deprecated: use StashBoxEndpoint Endpoint *int `json:"endpoint"` StashBoxEndpoint *string `json:"stash_box_endpoint"` // Fields to exclude when executing the tagging @@ -376,128 +404,143 @@ type StashBoxBatchTagInput struct { Refresh bool `json:"refresh"` // If batch adding studios, should their parent studios also be created? CreateParent bool `json:"createParent"` - // If set, only tag these ids + // IDs in stash of the items to update. + // If set, names and stash_ids fields will be ignored. Ids []string `json:"ids"` - // If set, only tag these names + // Names of the items in the stash-box instance to search for and create Names []string `json:"names"` - // If set, only tag these performer ids + // Stash IDs of the items in the stash-box instance to search for and create + StashIDs []string `json:"stash_ids"` + // IDs in stash of the performers to update // - // Deprecated: please use Ids + // Deprecated: use Ids PerformerIds []string `json:"performer_ids"` - // If set, only tag these performer names + // Names of the performers in the stash-box instance to search for and create // - // Deprecated: please use Names + // Deprecated: use Names PerformerNames []string `json:"performer_names"` } +func (s *Manager) batchTagPerformersByIds(ctx context.Context, input StashBoxBatchTagInput, box *models.StashBox) ([]Task, error) { + var tasks []Task + + err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { + performerQuery := s.Repository.Performer + + ids := input.Ids + if len(ids) == 0 { + ids = input.PerformerIds //nolint:staticcheck + } + + for _, performerID := range ids { + if id, err := strconv.Atoi(performerID); err == nil { + performer, err := performerQuery.Find(ctx, id) + if err != nil { + return err + } + + if err := performer.LoadStashIDs(ctx, performerQuery); err != nil { + return fmt.Errorf("loading performer stash ids: %w", err) + } + + hasStashID := performer.StashIDs.ForEndpoint(box.Endpoint) != nil + if (input.Refresh && hasStashID) || (!input.Refresh && !hasStashID) { + tasks = append(tasks, &stashBoxBatchPerformerTagTask{ + performer: performer, + box: box, + excludedFields: input.ExcludeFields, + }) + } + } + } + return nil + }) + + return tasks, err +} + +func (s *Manager) batchTagPerformersByNamesOrStashIds(input StashBoxBatchTagInput, box *models.StashBox) []Task { + var tasks []Task + + for i := range input.StashIDs { + stashID := input.StashIDs[i] + if len(stashID) > 0 { + tasks = append(tasks, &stashBoxBatchPerformerTagTask{ + stashID: &stashID, + box: box, + excludedFields: input.ExcludeFields, + }) + } + } + + names := input.Names + if len(names) == 0 { + names = input.PerformerNames //nolint:staticcheck + } + + for i := range names { + name := names[i] + if len(name) > 0 { + tasks = append(tasks, &stashBoxBatchPerformerTagTask{ + name: &name, + box: box, + excludedFields: input.ExcludeFields, + }) + } + } + + return tasks +} + +func (s *Manager) batchTagAllPerformers(ctx context.Context, input StashBoxBatchTagInput, box *models.StashBox) ([]Task, error) { + var tasks []Task + + err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { + performerQuery := s.Repository.Performer + var performers []*models.Performer + var err error + + performers, err = performerQuery.FindByStashIDStatus(ctx, input.Refresh, box.Endpoint) + + if err != nil { + return fmt.Errorf("error querying performers: %v", err) + } + + for _, performer := range performers { + if err := performer.LoadStashIDs(ctx, performerQuery); err != nil { + return fmt.Errorf("error loading stash ids for performer %s: %v", performer.Name, err) + } + + tasks = append(tasks, &stashBoxBatchPerformerTagTask{ + performer: performer, + box: box, + excludedFields: input.ExcludeFields, + }) + } + return nil + }) + + return tasks, err +} + func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, box *models.StashBox, input StashBoxBatchTagInput) int { j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error { logger.Infof("Initiating stash-box batch performer tag") - var tasks []StashBoxBatchTagTask + var tasks []Task + var err error - // The gocritic linter wants to turn this ifElseChain into a switch. - // however, such a switch would contain quite large blocks for each section - // and would arguably be hard to read. - // - // This is why we mark this section nolint. In principle, we should look to - // rewrite the section at some point, to avoid the linter warning. - if len(input.Ids) > 0 || len(input.PerformerIds) > 0 { //nolint:gocritic - // The user has chosen only to tag the items on the current page - if err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { - performerQuery := s.Repository.Performer + switch input.getBatchTagType(true) { + case batchTagByIds: + tasks, err = s.batchTagPerformersByIds(ctx, input, box) + case batchTagByNamesOrStashIds: + tasks = s.batchTagPerformersByNamesOrStashIds(input, box) + case batchTagAll: + tasks, err = s.batchTagAllPerformers(ctx, input, box) + } - idsToUse := input.PerformerIds - if len(input.Ids) > 0 { - idsToUse = input.Ids - } - - for _, performerID := range idsToUse { - if id, err := strconv.Atoi(performerID); err == nil { - performer, err := performerQuery.Find(ctx, id) - if err == nil { - if err := performer.LoadStashIDs(ctx, performerQuery); err != nil { - return fmt.Errorf("loading performer stash ids: %w", err) - } - - // Check if the user wants to refresh existing or new items - hasStashID := performer.StashIDs.ForEndpoint(box.Endpoint) != nil - if (input.Refresh && hasStashID) || (!input.Refresh && !hasStashID) { - tasks = append(tasks, StashBoxBatchTagTask{ - performer: performer, - refresh: input.Refresh, - box: box, - excludedFields: input.ExcludeFields, - taskType: Performer, - }) - } - } else { - return err - } - } - } - return nil - }); err != nil { - return err - } - } else if len(input.Names) > 0 || len(input.PerformerNames) > 0 { - // The user is batch adding performers - namesToUse := input.PerformerNames - if len(input.Names) > 0 { - namesToUse = input.Names - } - - for i := range namesToUse { - name := namesToUse[i] - if len(name) > 0 { - tasks = append(tasks, StashBoxBatchTagTask{ - name: &name, - refresh: false, - box: box, - excludedFields: input.ExcludeFields, - taskType: Performer, - }) - } - } - } else { //nolint:gocritic - // The gocritic linter wants to fold this if-block into the else on the line above. - // However, this doesn't really help with readability of the current section. Mark it - // as nolint for now. In the future we'd like to rewrite this code by factoring some of - // this into separate functions. - - // The user has chosen to tag every item in their database - if err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { - performerQuery := s.Repository.Performer - var performers []*models.Performer - var err error - - if input.Refresh { - performers, err = performerQuery.FindByStashIDStatus(ctx, true, box.Endpoint) - } else { - performers, err = performerQuery.FindByStashIDStatus(ctx, false, box.Endpoint) - } - - if err != nil { - return fmt.Errorf("error querying performers: %v", err) - } - - for _, performer := range performers { - if err := performer.LoadStashIDs(ctx, performerQuery); err != nil { - return fmt.Errorf("error loading stash ids for performer %s: %v", performer.Name, err) - } - - tasks = append(tasks, StashBoxBatchTagTask{ - performer: performer, - refresh: input.Refresh, - box: box, - excludedFields: input.ExcludeFields, - taskType: Performer, - }) - } - return nil - }); err != nil { - return err - } + if err != nil { + return err } if len(tasks) == 0 { @@ -509,7 +552,7 @@ func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, box *models.Sta logger.Infof("Starting stash-box batch operation for %d performers", len(tasks)) for _, task := range tasks { - progress.ExecuteTask(task.Description(), func() { + progress.ExecuteTask(task.GetDescription(), func() { task.Start(ctx) }) @@ -522,103 +565,116 @@ func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, box *models.Sta return s.JobManager.Add(ctx, "Batch stash-box performer tag...", j) } +func (s *Manager) batchTagStudiosByIds(ctx context.Context, input StashBoxBatchTagInput, box *models.StashBox) ([]Task, error) { + var tasks []Task + + err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { + studioQuery := s.Repository.Studio + + for _, studioID := range input.Ids { + if id, err := strconv.Atoi(studioID); err == nil { + studio, err := studioQuery.Find(ctx, id) + if err != nil { + return err + } + + if err := studio.LoadStashIDs(ctx, studioQuery); err != nil { + return fmt.Errorf("loading studio stash ids: %w", err) + } + + hasStashID := studio.StashIDs.ForEndpoint(box.Endpoint) != nil + if (input.Refresh && hasStashID) || (!input.Refresh && !hasStashID) { + tasks = append(tasks, &stashBoxBatchStudioTagTask{ + studio: studio, + createParent: input.CreateParent, + box: box, + excludedFields: input.ExcludeFields, + }) + } + } + } + return nil + }) + + return tasks, err +} + +func (s *Manager) batchTagStudiosByNamesOrStashIds(input StashBoxBatchTagInput, box *models.StashBox) []Task { + var tasks []Task + + for i := range input.StashIDs { + stashID := input.StashIDs[i] + if len(stashID) > 0 { + tasks = append(tasks, &stashBoxBatchStudioTagTask{ + stashID: &stashID, + createParent: input.CreateParent, + box: box, + excludedFields: input.ExcludeFields, + }) + } + } + + for i := range input.Names { + name := input.Names[i] + if len(name) > 0 { + tasks = append(tasks, &stashBoxBatchStudioTagTask{ + name: &name, + createParent: input.CreateParent, + box: box, + excludedFields: input.ExcludeFields, + }) + } + } + + return tasks +} + +func (s *Manager) batchTagAllStudios(ctx context.Context, input StashBoxBatchTagInput, box *models.StashBox) ([]Task, error) { + var tasks []Task + + err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { + studioQuery := s.Repository.Studio + var studios []*models.Studio + var err error + + studios, err = studioQuery.FindByStashIDStatus(ctx, input.Refresh, box.Endpoint) + + if err != nil { + return fmt.Errorf("error querying studios: %v", err) + } + + for _, studio := range studios { + tasks = append(tasks, &stashBoxBatchStudioTagTask{ + studio: studio, + createParent: input.CreateParent, + box: box, + excludedFields: input.ExcludeFields, + }) + } + return nil + }) + + return tasks, err +} + func (s *Manager) StashBoxBatchStudioTag(ctx context.Context, box *models.StashBox, input StashBoxBatchTagInput) int { j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error { logger.Infof("Initiating stash-box batch studio tag") - var tasks []StashBoxBatchTagTask + var tasks []Task + var err error - // The gocritic linter wants to turn this ifElseChain into a switch. - // however, such a switch would contain quite large blocks for each section - // and would arguably be hard to read. - // - // This is why we mark this section nolint. In principle, we should look to - // rewrite the section at some point, to avoid the linter warning. - if len(input.Ids) > 0 { //nolint:gocritic - // The user has chosen only to tag the items on the current page - if err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { - studioQuery := s.Repository.Studio + switch input.getBatchTagType(false) { + case batchTagByIds: + tasks, err = s.batchTagStudiosByIds(ctx, input, box) + case batchTagByNamesOrStashIds: + tasks = s.batchTagStudiosByNamesOrStashIds(input, box) + case batchTagAll: + tasks, err = s.batchTagAllStudios(ctx, input, box) + } - for _, studioID := range input.Ids { - if id, err := strconv.Atoi(studioID); err == nil { - studio, err := studioQuery.Find(ctx, id) - if err == nil { - if err := studio.LoadStashIDs(ctx, studioQuery); err != nil { - return fmt.Errorf("loading studio stash ids: %w", err) - } - - // Check if the user wants to refresh existing or new items - hasStashID := studio.StashIDs.ForEndpoint(box.Endpoint) != nil - if (input.Refresh && hasStashID) || (!input.Refresh && !hasStashID) { - tasks = append(tasks, StashBoxBatchTagTask{ - studio: studio, - refresh: input.Refresh, - createParent: input.CreateParent, - box: box, - excludedFields: input.ExcludeFields, - taskType: Studio, - }) - } - } else { - return err - } - } - } - return nil - }); err != nil { - logger.Error(err.Error()) - } - } else if len(input.Names) > 0 { - // The user is batch adding studios - for i := range input.Names { - name := input.Names[i] - if len(name) > 0 { - tasks = append(tasks, StashBoxBatchTagTask{ - name: &name, - refresh: false, - createParent: input.CreateParent, - box: box, - excludedFields: input.ExcludeFields, - taskType: Studio, - }) - } - } - } else { //nolint:gocritic - // The gocritic linter wants to fold this if-block into the else on the line above. - // However, this doesn't really help with readability of the current section. Mark it - // as nolint for now. In the future we'd like to rewrite this code by factoring some of - // this into separate functions. - - // The user has chosen to tag every item in their database - if err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { - studioQuery := s.Repository.Studio - var studios []*models.Studio - var err error - - if input.Refresh { - studios, err = studioQuery.FindByStashIDStatus(ctx, true, box.Endpoint) - } else { - studios, err = studioQuery.FindByStashIDStatus(ctx, false, box.Endpoint) - } - - if err != nil { - return fmt.Errorf("error querying studios: %v", err) - } - - for _, studio := range studios { - tasks = append(tasks, StashBoxBatchTagTask{ - studio: studio, - refresh: input.Refresh, - createParent: input.CreateParent, - box: box, - excludedFields: input.ExcludeFields, - taskType: Studio, - }) - } - return nil - }); err != nil { - return err - } + if err != nil { + return err } if len(tasks) == 0 { @@ -630,7 +686,7 @@ func (s *Manager) StashBoxBatchStudioTag(ctx context.Context, box *models.StashB logger.Infof("Starting stash-box batch operation for %d studios", len(tasks)) for _, task := range tasks { - progress.ExecuteTask(task.Description(), func() { + progress.ExecuteTask(task.GetDescription(), func() { task.Start(ctx) }) diff --git a/internal/manager/task_stash_box_tag.go b/internal/manager/task_stash_box_tag.go index d20b71f06..d7d987a6d 100644 --- a/internal/manager/task_stash_box_tag.go +++ b/internal/manager/task_stash_box_tag.go @@ -14,57 +14,33 @@ import ( "github.com/stashapp/stash/pkg/studio" ) -type StashBoxTagTaskType int - -const ( - Performer StashBoxTagTaskType = iota - Studio -) - -type StashBoxBatchTagTask struct { +// stashBoxBatchPerformerTagTask is used to tag or create performers from stash-box. +// +// Two modes of operation: +// - Update existing performer: set performer to update from stash-box data +// - Create new performer: set name or stashID to search stash-box and create locally +type stashBoxBatchPerformerTagTask struct { box *models.StashBox name *string + stashID *string performer *models.Performer - studio *models.Studio - refresh bool - createParent bool excludedFields []string - taskType StashBoxTagTaskType } -func (t *StashBoxBatchTagTask) Start(ctx context.Context) { - switch t.taskType { - case Performer: - t.stashBoxPerformerTag(ctx) - case Studio: - t.stashBoxStudioTag(ctx) +func (t *stashBoxBatchPerformerTagTask) getName() string { + switch { + case t.name != nil: + return *t.name + case t.stashID != nil: + return *t.stashID + case t.performer != nil: + return t.performer.Name default: - logger.Errorf("Error starting batch task, unknown task_type %d", t.taskType) + return "" } } -func (t *StashBoxBatchTagTask) Description() string { - if t.taskType == Performer { - var name string - if t.name != nil { - name = *t.name - } else { - name = t.performer.Name - } - return fmt.Sprintf("Tagging performer %s from stash-box", name) - } else if t.taskType == Studio { - var name string - if t.name != nil { - name = *t.name - } else { - name = t.studio.Name - } - return fmt.Sprintf("Tagging studio %s from stash-box", name) - } - return fmt.Sprintf("Unknown tagging task type %d from stash-box", t.taskType) -} - -func (t *StashBoxBatchTagTask) stashBoxPerformerTag(ctx context.Context) { +func (t *stashBoxBatchPerformerTagTask) Start(ctx context.Context) { performer, err := t.findStashBoxPerformer(ctx) if err != nil { logger.Errorf("Error fetching performer data from stash-box: %v", err) @@ -76,21 +52,18 @@ func (t *StashBoxBatchTagTask) stashBoxPerformerTag(ctx context.Context) { excluded[field] = true } - // performer will have a value if pulling from Stash-box by Stash ID or name was successful if performer != nil { t.processMatchedPerformer(ctx, performer, excluded) } else { - var name string - if t.name != nil { - name = *t.name - } else if t.performer != nil { - name = t.performer.Name - } - logger.Infof("No match found for %s", name) + logger.Infof("No match found for %s", t.getName()) } } -func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*models.ScrapedPerformer, error) { +func (t *stashBoxBatchPerformerTagTask) GetDescription() string { + return fmt.Sprintf("Tagging performer %s from stash-box", t.getName()) +} + +func (t *stashBoxBatchPerformerTagTask) findStashBoxPerformer(ctx context.Context) (*models.ScrapedPerformer, error) { var performer *models.ScrapedPerformer var err error @@ -98,7 +71,24 @@ func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*mode client := stashbox.NewClient(*t.box, stashbox.ExcludeTagPatterns(instance.Config.GetScraperExcludeTagPatterns())) - if t.refresh { + switch { + case t.name != nil: + performer, err = client.FindPerformerByName(ctx, *t.name) + case t.stashID != nil: + performer, err = client.FindPerformerByID(ctx, *t.stashID) + + if performer != nil && performer.RemoteMergedIntoId != nil { + mergedPerformer, err := t.handleMergedPerformer(ctx, performer, client) + if err != nil { + return nil, err + } + + if mergedPerformer != nil { + logger.Infof("Performer id %s merged into %s, updating local performer", *t.stashID, *performer.RemoteMergedIntoId) + performer = mergedPerformer + } + } + case t.performer != nil: var remoteID string if err := r.WithReadTxn(ctx, func(ctx context.Context) error { qb := r.Performer @@ -118,6 +108,7 @@ func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*mode }); err != nil { return nil, err } + if remoteID != "" { performer, err = client.FindPerformerByID(ctx, remoteID) @@ -133,14 +124,6 @@ func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*mode } } } - } else { - var name string - if t.name != nil { - name = *t.name - } else { - name = t.performer.Name - } - performer, err = client.FindPerformerByName(ctx, name) } if performer != nil { @@ -154,7 +137,7 @@ func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*mode return performer, err } -func (t *StashBoxBatchTagTask) handleMergedPerformer(ctx context.Context, performer *models.ScrapedPerformer, client *stashbox.Client) (mergedPerformer *models.ScrapedPerformer, err error) { +func (t *stashBoxBatchPerformerTagTask) handleMergedPerformer(ctx context.Context, performer *models.ScrapedPerformer, client *stashbox.Client) (mergedPerformer *models.ScrapedPerformer, err error) { mergedPerformer, err = client.FindPerformerByID(ctx, *performer.RemoteMergedIntoId) if err != nil { return nil, fmt.Errorf("loading merged performer %s from stashbox", *performer.RemoteMergedIntoId) @@ -169,8 +152,7 @@ func (t *StashBoxBatchTagTask) handleMergedPerformer(ctx context.Context, perfor return mergedPerformer, nil } -func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *models.ScrapedPerformer, excluded map[string]bool) { - // Refreshing an existing performer +func (t *stashBoxBatchPerformerTagTask) processMatchedPerformer(ctx context.Context, p *models.ScrapedPerformer, excluded map[string]bool) { if t.performer != nil { storedID, _ := strconv.Atoi(*p.StoredID) @@ -180,7 +162,6 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m return } - // Start the transaction and update the performer r := instance.Repository err = r.WithTxn(ctx, func(ctx context.Context) error { qb := r.Performer @@ -226,8 +207,8 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m } else { logger.Infof("Updated performer %s", *p.Name) } - } else if t.name != nil && p.Name != nil { - // Creating a new performer + } else { + // no existing performer, create a new one newPerformer := p.ToPerformer(t.box.Endpoint, excluded) image, err := p.GetImage(ctx, excluded) if err != nil { @@ -263,7 +244,34 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m } } -func (t *StashBoxBatchTagTask) stashBoxStudioTag(ctx context.Context) { +// stashBoxBatchStudioTagTask is used to tag or create studios from stash-box. +// +// Two modes of operation: +// - Update existing studio: set studio to update from stash-box data +// - Create new studio: set name or stashID to search stash-box and create locally +type stashBoxBatchStudioTagTask struct { + box *models.StashBox + name *string + stashID *string + studio *models.Studio + createParent bool + excludedFields []string +} + +func (t *stashBoxBatchStudioTagTask) getName() string { + switch { + case t.name != nil: + return *t.name + case t.stashID != nil: + return *t.stashID + case t.studio != nil: + return t.studio.Name + default: + return "" + } +} + +func (t *stashBoxBatchStudioTagTask) Start(ctx context.Context) { studio, err := t.findStashBoxStudio(ctx) if err != nil { logger.Errorf("Error fetching studio data from stash-box: %v", err) @@ -275,21 +283,18 @@ func (t *StashBoxBatchTagTask) stashBoxStudioTag(ctx context.Context) { excluded[field] = true } - // studio will have a value if pulling from Stash-box by Stash ID or name was successful if studio != nil { t.processMatchedStudio(ctx, studio, excluded) } else { - var name string - if t.name != nil { - name = *t.name - } else if t.studio != nil { - name = t.studio.Name - } - logger.Infof("No match found for %s", name) + logger.Infof("No match found for %s", t.getName()) } } -func (t *StashBoxBatchTagTask) findStashBoxStudio(ctx context.Context) (*models.ScrapedStudio, error) { +func (t *stashBoxBatchStudioTagTask) GetDescription() string { + return fmt.Sprintf("Tagging studio %s from stash-box", t.getName()) +} + +func (t *stashBoxBatchStudioTagTask) findStashBoxStudio(ctx context.Context) (*models.ScrapedStudio, error) { var studio *models.ScrapedStudio var err error @@ -297,7 +302,12 @@ func (t *StashBoxBatchTagTask) findStashBoxStudio(ctx context.Context) (*models. client := stashbox.NewClient(*t.box, stashbox.ExcludeTagPatterns(instance.Config.GetScraperExcludeTagPatterns())) - if t.refresh { + switch { + case t.name != nil: + studio, err = client.FindStudio(ctx, *t.name) + case t.stashID != nil: + studio, err = client.FindStudio(ctx, *t.stashID) + case t.studio != nil: var remoteID string if err := r.WithReadTxn(ctx, func(ctx context.Context) error { if !t.studio.StashIDs.Loaded() { @@ -315,17 +325,10 @@ func (t *StashBoxBatchTagTask) findStashBoxStudio(ctx context.Context) (*models. }); err != nil { return nil, err } + if remoteID != "" { studio, err = client.FindStudio(ctx, remoteID) } - } else { - var name string - if t.name != nil { - name = *t.name - } else { - name = t.studio.Name - } - studio, err = client.FindStudio(ctx, name) } if err := r.WithReadTxn(ctx, func(ctx context.Context) error { @@ -343,8 +346,7 @@ func (t *StashBoxBatchTagTask) findStashBoxStudio(ctx context.Context) (*models. return studio, err } -func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *models.ScrapedStudio, excluded map[string]bool) { - // Refreshing an existing studio +func (t *stashBoxBatchStudioTagTask) processMatchedStudio(ctx context.Context, s *models.ScrapedStudio, excluded map[string]bool) { if t.studio != nil { storedID, _ := strconv.Atoi(*s.StoredID) @@ -361,7 +363,6 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode return } - // Start the transaction and update the studio r := instance.Repository err = r.WithTxn(ctx, func(ctx context.Context) error { qb := r.Studio @@ -394,8 +395,8 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode } else { logger.Infof("Updated studio %s", s.Name) } - } else if t.name != nil && s.Name != "" { - // Creating a new studio + } else if s.Name != "" { + // no existing studio, create a new one if s.Parent != nil && t.createParent { err := t.processParentStudio(ctx, s.Parent, excluded) if err != nil { @@ -410,7 +411,6 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode return } - // Start the transaction and save the studio r := instance.Repository err = r.WithTxn(ctx, func(ctx context.Context) error { qb := r.Studio @@ -439,9 +439,8 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode } } -func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent *models.ScrapedStudio, excluded map[string]bool) error { +func (t *stashBoxBatchStudioTagTask) processParentStudio(ctx context.Context, parent *models.ScrapedStudio, excluded map[string]bool) error { if parent.StoredID == nil { - // The parent needs to be created newParentStudio := parent.ToStudio(t.box.Endpoint, excluded) image, err := parent.GetImage(ctx, excluded) @@ -450,7 +449,6 @@ func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent * return err } - // Start the transaction and save the studio r := instance.Repository err = r.WithTxn(ctx, func(ctx context.Context) error { qb := r.Studio @@ -476,7 +474,6 @@ func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent * } return err } else { - // The parent studio matched an existing one and the user has chosen in the UI to link and/or update it storedID, _ := strconv.Atoi(*parent.StoredID) image, err := parent.GetImage(ctx, excluded) @@ -485,7 +482,6 @@ func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent * return err } - // Start the transaction and update the studio r := instance.Repository err = r.WithTxn(ctx, func(ctx context.Context) error { qb := r.Studio diff --git a/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx b/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx index a6e2bcd1c..bb934a241 100755 --- a/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx +++ b/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx @@ -25,6 +25,7 @@ import PerformerModal from "../PerformerModal"; import { useUpdatePerformer } from "../queries"; import { faStar, faTags } from "@fortawesome/free-solid-svg-icons"; import { mergeStashIDs } from "src/utils/stashbox"; +import { separateNamesAndStashIds } from "src/utils/stashIds"; import { ExternalLink } from "src/components/Shared/ExternalLink"; import { useTaggerConfig } from "../config"; @@ -222,7 +223,7 @@ const PerformerBatchAddModal: React.FC = ({ as="textarea" ref={performerInput} placeholder={intl.formatMessage({ - id: "performer_tagger.performer_names_separated_by_comma", + id: "performer_tagger.performer_names_or_stashids_separated_by_comma", })} rows={6} /> @@ -666,14 +667,17 @@ export const PerformerTagger: React.FC = ({ performers }) => { async function batchAdd(performerInput: string) { if (performerInput && selectedEndpoint) { - const names = performerInput + const inputs = performerInput .split(",") .map((n) => n.trim()) .filter((n) => n.length > 0); - if (names.length > 0) { + const { names, stashIds } = separateNamesAndStashIds(inputs); + + if (names.length > 0 || stashIds.length > 0) { const ret = await mutateStashBoxBatchPerformerTag({ - names: names, + names: names.length > 0 ? names : undefined, + stash_ids: stashIds.length > 0 ? stashIds : undefined, endpoint: selectedEndpointIndex, refresh: false, createParent: false, diff --git a/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx b/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx index 78553e518..ed9570431 100644 --- a/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx +++ b/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx @@ -28,6 +28,7 @@ import { apolloError } from "src/utils"; import { faStar, faTags } from "@fortawesome/free-solid-svg-icons"; import { ExternalLink } from "src/components/Shared/ExternalLink"; import { mergeStudioStashIDs } from "../utils"; +import { separateNamesAndStashIds } from "src/utils/stashIds"; import { useTaggerConfig } from "../config"; type JobFragment = Pick< @@ -242,7 +243,7 @@ const StudioBatchAddModal: React.FC = ({ as="textarea" ref={studioInput} placeholder={intl.formatMessage({ - id: "studio_tagger.studio_names_separated_by_comma", + id: "studio_tagger.studio_names_or_stashids_separated_by_comma", })} rows={6} /> @@ -715,14 +716,17 @@ export const StudioTagger: React.FC = ({ studios }) => { async function batchAdd(studioInput: string, createParent: boolean) { if (studioInput && selectedEndpoint) { - const names = studioInput + const inputs = studioInput .split(",") .map((n) => n.trim()) .filter((n) => n.length > 0); - if (names.length > 0) { + const { names, stashIds } = separateNamesAndStashIds(inputs); + + if (names.length > 0 || stashIds.length > 0) { const ret = await mutateStashBoxBatchStudioTag({ - names: names, + names: names.length > 0 ? names : undefined, + stash_ids: stashIds.length > 0 ? stashIds : undefined, endpoint: selectedEndpointIndex, refresh: false, exclude_fields: config?.excludedStudioFields ?? [], diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 89ec4984c..be50fc8da 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -1295,7 +1295,7 @@ "no_results_found": "No results found.", "number_of_performers_will_be_processed": "{performer_count} performers will be processed", "performer_already_tagged": "Performer already tagged", - "performer_names_separated_by_comma": "Performer names separated by comma", + "performer_names_or_stashids_separated_by_comma": "Performer names or StashIDs separated by comma", "performer_selection": "Performer selection", "performer_successfully_tagged": "Performer successfully tagged:", "query_all_performers_in_the_database": "All performers in the database", @@ -1510,7 +1510,7 @@ "status_tagging_job_queued": "Status: Tagging job queued", "status_tagging_studios": "Status: Tagging studios", "studio_already_tagged": "Studio already tagged", - "studio_names_separated_by_comma": "Studio names separated by comma", + "studio_names_or_stashids_separated_by_comma": "Studio names or StashIDs separated by comma", "studio_selection": "Studio selection", "studio_successfully_tagged": "Studio successfully tagged", "tag_status": "Tag Status", diff --git a/ui/v2.5/src/utils/stashIds.ts b/ui/v2.5/src/utils/stashIds.ts index 289ce9c9d..f44b182ab 100644 --- a/ui/v2.5/src/utils/stashIds.ts +++ b/ui/v2.5/src/utils/stashIds.ts @@ -6,3 +6,29 @@ export const getStashIDs = ( endpoint, updated_at, })); + +// UUID regex pattern to detect StashIDs (supports v4 and v7) +const UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[47][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +/** + * Separates a list of inputs into names and StashIDs based on UUID pattern matching + * @param inputs - Array of strings that could be either names or StashIDs + * @returns Object containing separate arrays for names and stashIds + */ +export const separateNamesAndStashIds = ( + inputs: string[] +): { names: string[]; stashIds: string[] } => { + const names: string[] = []; + const stashIds: string[] = []; + + inputs.forEach((input) => { + if (UUID_PATTERN.test(input)) { + stashIds.push(input); + } else { + names.push(input); + } + }); + + return { names, stashIds }; +};