mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Feature Request: Bulk Add by StashID and Name (#6310)
This commit is contained in:
parent
7e66ce8a49
commit
e052a431d1
8 changed files with 409 additions and 315 deletions
|
|
@ -281,7 +281,10 @@ type StashBoxFingerprint {
|
||||||
duration: Int!
|
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 {
|
input StashBoxBatchTagInput {
|
||||||
"Stash endpoint to use for the tagging"
|
"Stash endpoint to use for the tagging"
|
||||||
endpoint: Int @deprecated(reason: "use stash_box_endpoint")
|
endpoint: Int @deprecated(reason: "use stash_box_endpoint")
|
||||||
|
|
@ -293,12 +296,17 @@ input StashBoxBatchTagInput {
|
||||||
refresh: Boolean!
|
refresh: Boolean!
|
||||||
"If batch adding studios, should their parent studios also be created?"
|
"If batch adding studios, should their parent studios also be created?"
|
||||||
createParent: Boolean!
|
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!]
|
ids: [ID!]
|
||||||
"If set, only tag these names"
|
"Names of the items in the stash-box instance to search for and create"
|
||||||
names: [String!]
|
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")
|
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")
|
performer_names: [String!] @deprecated(reason: "use names")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input manager.StashBoxBatchTagInput) (string, error) {
|
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 {
|
if err != nil {
|
||||||
return "", err
|
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) {
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -365,9 +365,37 @@ func (s *Manager) MigrateHash(ctx context.Context) int {
|
||||||
return s.JobManager.Add(ctx, "Migrating scene hashes...", j)
|
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 {
|
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"`
|
Endpoint *int `json:"endpoint"`
|
||||||
StashBoxEndpoint *string `json:"stash_box_endpoint"`
|
StashBoxEndpoint *string `json:"stash_box_endpoint"`
|
||||||
// Fields to exclude when executing the tagging
|
// Fields to exclude when executing the tagging
|
||||||
|
|
@ -376,128 +404,143 @@ type StashBoxBatchTagInput struct {
|
||||||
Refresh bool `json:"refresh"`
|
Refresh bool `json:"refresh"`
|
||||||
// If batch adding studios, should their parent studios also be created?
|
// If batch adding studios, should their parent studios also be created?
|
||||||
CreateParent bool `json:"createParent"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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 {
|
func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, box *models.StashBox, input StashBoxBatchTagInput) int {
|
||||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||||
logger.Infof("Initiating stash-box batch performer tag")
|
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.
|
switch input.getBatchTagType(true) {
|
||||||
// however, such a switch would contain quite large blocks for each section
|
case batchTagByIds:
|
||||||
// and would arguably be hard to read.
|
tasks, err = s.batchTagPerformersByIds(ctx, input, box)
|
||||||
//
|
case batchTagByNamesOrStashIds:
|
||||||
// This is why we mark this section nolint. In principle, we should look to
|
tasks = s.batchTagPerformersByNamesOrStashIds(input, box)
|
||||||
// rewrite the section at some point, to avoid the linter warning.
|
case batchTagAll:
|
||||||
if len(input.Ids) > 0 || len(input.PerformerIds) > 0 { //nolint:gocritic
|
tasks, err = s.batchTagAllPerformers(ctx, input, box)
|
||||||
// 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
|
|
||||||
|
|
||||||
idsToUse := input.PerformerIds
|
if err != nil {
|
||||||
if len(input.Ids) > 0 {
|
return err
|
||||||
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 len(tasks) == 0 {
|
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))
|
logger.Infof("Starting stash-box batch operation for %d performers", len(tasks))
|
||||||
|
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
progress.ExecuteTask(task.Description(), func() {
|
progress.ExecuteTask(task.GetDescription(), func() {
|
||||||
task.Start(ctx)
|
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)
|
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 {
|
func (s *Manager) StashBoxBatchStudioTag(ctx context.Context, box *models.StashBox, input StashBoxBatchTagInput) int {
|
||||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||||
logger.Infof("Initiating stash-box batch studio tag")
|
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.
|
switch input.getBatchTagType(false) {
|
||||||
// however, such a switch would contain quite large blocks for each section
|
case batchTagByIds:
|
||||||
// and would arguably be hard to read.
|
tasks, err = s.batchTagStudiosByIds(ctx, input, box)
|
||||||
//
|
case batchTagByNamesOrStashIds:
|
||||||
// This is why we mark this section nolint. In principle, we should look to
|
tasks = s.batchTagStudiosByNamesOrStashIds(input, box)
|
||||||
// rewrite the section at some point, to avoid the linter warning.
|
case batchTagAll:
|
||||||
if len(input.Ids) > 0 { //nolint:gocritic
|
tasks, err = s.batchTagAllStudios(ctx, input, box)
|
||||||
// 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
|
|
||||||
|
|
||||||
for _, studioID := range input.Ids {
|
if err != nil {
|
||||||
if id, err := strconv.Atoi(studioID); err == nil {
|
return err
|
||||||
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 len(tasks) == 0 {
|
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))
|
logger.Infof("Starting stash-box batch operation for %d studios", len(tasks))
|
||||||
|
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
progress.ExecuteTask(task.Description(), func() {
|
progress.ExecuteTask(task.GetDescription(), func() {
|
||||||
task.Start(ctx)
|
task.Start(ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,57 +14,33 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/studio"
|
"github.com/stashapp/stash/pkg/studio"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StashBoxTagTaskType int
|
// stashBoxBatchPerformerTagTask is used to tag or create performers from stash-box.
|
||||||
|
//
|
||||||
const (
|
// Two modes of operation:
|
||||||
Performer StashBoxTagTaskType = iota
|
// - Update existing performer: set performer to update from stash-box data
|
||||||
Studio
|
// - Create new performer: set name or stashID to search stash-box and create locally
|
||||||
)
|
type stashBoxBatchPerformerTagTask struct {
|
||||||
|
|
||||||
type StashBoxBatchTagTask struct {
|
|
||||||
box *models.StashBox
|
box *models.StashBox
|
||||||
name *string
|
name *string
|
||||||
|
stashID *string
|
||||||
performer *models.Performer
|
performer *models.Performer
|
||||||
studio *models.Studio
|
|
||||||
refresh bool
|
|
||||||
createParent bool
|
|
||||||
excludedFields []string
|
excludedFields []string
|
||||||
taskType StashBoxTagTaskType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *StashBoxBatchTagTask) Start(ctx context.Context) {
|
func (t *stashBoxBatchPerformerTagTask) getName() string {
|
||||||
switch t.taskType {
|
switch {
|
||||||
case Performer:
|
case t.name != nil:
|
||||||
t.stashBoxPerformerTag(ctx)
|
return *t.name
|
||||||
case Studio:
|
case t.stashID != nil:
|
||||||
t.stashBoxStudioTag(ctx)
|
return *t.stashID
|
||||||
|
case t.performer != nil:
|
||||||
|
return t.performer.Name
|
||||||
default:
|
default:
|
||||||
logger.Errorf("Error starting batch task, unknown task_type %d", t.taskType)
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *StashBoxBatchTagTask) Description() string {
|
func (t *stashBoxBatchPerformerTagTask) Start(ctx context.Context) {
|
||||||
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) {
|
|
||||||
performer, err := t.findStashBoxPerformer(ctx)
|
performer, err := t.findStashBoxPerformer(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Error fetching performer data from stash-box: %v", err)
|
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
|
excluded[field] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// performer will have a value if pulling from Stash-box by Stash ID or name was successful
|
|
||||||
if performer != nil {
|
if performer != nil {
|
||||||
t.processMatchedPerformer(ctx, performer, excluded)
|
t.processMatchedPerformer(ctx, performer, excluded)
|
||||||
} else {
|
} else {
|
||||||
var name string
|
logger.Infof("No match found for %s", t.getName())
|
||||||
if t.name != nil {
|
|
||||||
name = *t.name
|
|
||||||
} else if t.performer != nil {
|
|
||||||
name = t.performer.Name
|
|
||||||
}
|
|
||||||
logger.Infof("No match found for %s", name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 performer *models.ScrapedPerformer
|
||||||
var err error
|
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()))
|
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
|
var remoteID string
|
||||||
if err := r.WithReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.WithReadTxn(ctx, func(ctx context.Context) error {
|
||||||
qb := r.Performer
|
qb := r.Performer
|
||||||
|
|
@ -118,6 +108,7 @@ func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*mode
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if remoteID != "" {
|
if remoteID != "" {
|
||||||
performer, err = client.FindPerformerByID(ctx, 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 {
|
if performer != nil {
|
||||||
|
|
@ -154,7 +137,7 @@ func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*mode
|
||||||
return performer, err
|
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)
|
mergedPerformer, err = client.FindPerformerByID(ctx, *performer.RemoteMergedIntoId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("loading merged performer %s from stashbox", *performer.RemoteMergedIntoId)
|
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
|
return mergedPerformer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *models.ScrapedPerformer, excluded map[string]bool) {
|
func (t *stashBoxBatchPerformerTagTask) processMatchedPerformer(ctx context.Context, p *models.ScrapedPerformer, excluded map[string]bool) {
|
||||||
// Refreshing an existing performer
|
|
||||||
if t.performer != nil {
|
if t.performer != nil {
|
||||||
storedID, _ := strconv.Atoi(*p.StoredID)
|
storedID, _ := strconv.Atoi(*p.StoredID)
|
||||||
|
|
||||||
|
|
@ -180,7 +162,6 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the transaction and update the performer
|
|
||||||
r := instance.Repository
|
r := instance.Repository
|
||||||
err = r.WithTxn(ctx, func(ctx context.Context) error {
|
err = r.WithTxn(ctx, func(ctx context.Context) error {
|
||||||
qb := r.Performer
|
qb := r.Performer
|
||||||
|
|
@ -226,8 +207,8 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("Updated performer %s", *p.Name)
|
logger.Infof("Updated performer %s", *p.Name)
|
||||||
}
|
}
|
||||||
} else if t.name != nil && p.Name != nil {
|
} else {
|
||||||
// Creating a new performer
|
// no existing performer, create a new one
|
||||||
newPerformer := p.ToPerformer(t.box.Endpoint, excluded)
|
newPerformer := p.ToPerformer(t.box.Endpoint, excluded)
|
||||||
image, err := p.GetImage(ctx, excluded)
|
image, err := p.GetImage(ctx, excluded)
|
||||||
if err != nil {
|
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)
|
studio, err := t.findStashBoxStudio(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Error fetching studio data from stash-box: %v", err)
|
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
|
excluded[field] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// studio will have a value if pulling from Stash-box by Stash ID or name was successful
|
|
||||||
if studio != nil {
|
if studio != nil {
|
||||||
t.processMatchedStudio(ctx, studio, excluded)
|
t.processMatchedStudio(ctx, studio, excluded)
|
||||||
} else {
|
} else {
|
||||||
var name string
|
logger.Infof("No match found for %s", t.getName())
|
||||||
if t.name != nil {
|
|
||||||
name = *t.name
|
|
||||||
} else if t.studio != nil {
|
|
||||||
name = t.studio.Name
|
|
||||||
}
|
|
||||||
logger.Infof("No match found for %s", name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 studio *models.ScrapedStudio
|
||||||
var err error
|
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()))
|
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
|
var remoteID string
|
||||||
if err := r.WithReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.WithReadTxn(ctx, func(ctx context.Context) error {
|
||||||
if !t.studio.StashIDs.Loaded() {
|
if !t.studio.StashIDs.Loaded() {
|
||||||
|
|
@ -315,17 +325,10 @@ func (t *StashBoxBatchTagTask) findStashBoxStudio(ctx context.Context) (*models.
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if remoteID != "" {
|
if remoteID != "" {
|
||||||
studio, err = client.FindStudio(ctx, 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 {
|
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
|
return studio, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *models.ScrapedStudio, excluded map[string]bool) {
|
func (t *stashBoxBatchStudioTagTask) processMatchedStudio(ctx context.Context, s *models.ScrapedStudio, excluded map[string]bool) {
|
||||||
// Refreshing an existing studio
|
|
||||||
if t.studio != nil {
|
if t.studio != nil {
|
||||||
storedID, _ := strconv.Atoi(*s.StoredID)
|
storedID, _ := strconv.Atoi(*s.StoredID)
|
||||||
|
|
||||||
|
|
@ -361,7 +363,6 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the transaction and update the studio
|
|
||||||
r := instance.Repository
|
r := instance.Repository
|
||||||
err = r.WithTxn(ctx, func(ctx context.Context) error {
|
err = r.WithTxn(ctx, func(ctx context.Context) error {
|
||||||
qb := r.Studio
|
qb := r.Studio
|
||||||
|
|
@ -394,8 +395,8 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("Updated studio %s", s.Name)
|
logger.Infof("Updated studio %s", s.Name)
|
||||||
}
|
}
|
||||||
} else if t.name != nil && s.Name != "" {
|
} else if s.Name != "" {
|
||||||
// Creating a new studio
|
// no existing studio, create a new one
|
||||||
if s.Parent != nil && t.createParent {
|
if s.Parent != nil && t.createParent {
|
||||||
err := t.processParentStudio(ctx, s.Parent, excluded)
|
err := t.processParentStudio(ctx, s.Parent, excluded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -410,7 +411,6 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the transaction and save the studio
|
|
||||||
r := instance.Repository
|
r := instance.Repository
|
||||||
err = r.WithTxn(ctx, func(ctx context.Context) error {
|
err = r.WithTxn(ctx, func(ctx context.Context) error {
|
||||||
qb := r.Studio
|
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 {
|
if parent.StoredID == nil {
|
||||||
// The parent needs to be created
|
|
||||||
newParentStudio := parent.ToStudio(t.box.Endpoint, excluded)
|
newParentStudio := parent.ToStudio(t.box.Endpoint, excluded)
|
||||||
|
|
||||||
image, err := parent.GetImage(ctx, excluded)
|
image, err := parent.GetImage(ctx, excluded)
|
||||||
|
|
@ -450,7 +449,6 @@ func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent *
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the transaction and save the studio
|
|
||||||
r := instance.Repository
|
r := instance.Repository
|
||||||
err = r.WithTxn(ctx, func(ctx context.Context) error {
|
err = r.WithTxn(ctx, func(ctx context.Context) error {
|
||||||
qb := r.Studio
|
qb := r.Studio
|
||||||
|
|
@ -476,7 +474,6 @@ func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent *
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
} else {
|
} 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)
|
storedID, _ := strconv.Atoi(*parent.StoredID)
|
||||||
|
|
||||||
image, err := parent.GetImage(ctx, excluded)
|
image, err := parent.GetImage(ctx, excluded)
|
||||||
|
|
@ -485,7 +482,6 @@ func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent *
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the transaction and update the studio
|
|
||||||
r := instance.Repository
|
r := instance.Repository
|
||||||
err = r.WithTxn(ctx, func(ctx context.Context) error {
|
err = r.WithTxn(ctx, func(ctx context.Context) error {
|
||||||
qb := r.Studio
|
qb := r.Studio
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import PerformerModal from "../PerformerModal";
|
||||||
import { useUpdatePerformer } from "../queries";
|
import { useUpdatePerformer } from "../queries";
|
||||||
import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
|
import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { mergeStashIDs } from "src/utils/stashbox";
|
import { mergeStashIDs } from "src/utils/stashbox";
|
||||||
|
import { separateNamesAndStashIds } from "src/utils/stashIds";
|
||||||
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||||
import { useTaggerConfig } from "../config";
|
import { useTaggerConfig } from "../config";
|
||||||
|
|
||||||
|
|
@ -222,7 +223,7 @@ const PerformerBatchAddModal: React.FC<IPerformerBatchAddModal> = ({
|
||||||
as="textarea"
|
as="textarea"
|
||||||
ref={performerInput}
|
ref={performerInput}
|
||||||
placeholder={intl.formatMessage({
|
placeholder={intl.formatMessage({
|
||||||
id: "performer_tagger.performer_names_separated_by_comma",
|
id: "performer_tagger.performer_names_or_stashids_separated_by_comma",
|
||||||
})}
|
})}
|
||||||
rows={6}
|
rows={6}
|
||||||
/>
|
/>
|
||||||
|
|
@ -666,14 +667,17 @@ export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => {
|
||||||
|
|
||||||
async function batchAdd(performerInput: string) {
|
async function batchAdd(performerInput: string) {
|
||||||
if (performerInput && selectedEndpoint) {
|
if (performerInput && selectedEndpoint) {
|
||||||
const names = performerInput
|
const inputs = performerInput
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((n) => n.trim())
|
.map((n) => n.trim())
|
||||||
.filter((n) => n.length > 0);
|
.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({
|
const ret = await mutateStashBoxBatchPerformerTag({
|
||||||
names: names,
|
names: names.length > 0 ? names : undefined,
|
||||||
|
stash_ids: stashIds.length > 0 ? stashIds : undefined,
|
||||||
endpoint: selectedEndpointIndex,
|
endpoint: selectedEndpointIndex,
|
||||||
refresh: false,
|
refresh: false,
|
||||||
createParent: false,
|
createParent: false,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import { apolloError } from "src/utils";
|
||||||
import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
|
import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||||
import { mergeStudioStashIDs } from "../utils";
|
import { mergeStudioStashIDs } from "../utils";
|
||||||
|
import { separateNamesAndStashIds } from "src/utils/stashIds";
|
||||||
import { useTaggerConfig } from "../config";
|
import { useTaggerConfig } from "../config";
|
||||||
|
|
||||||
type JobFragment = Pick<
|
type JobFragment = Pick<
|
||||||
|
|
@ -242,7 +243,7 @@ const StudioBatchAddModal: React.FC<IStudioBatchAddModal> = ({
|
||||||
as="textarea"
|
as="textarea"
|
||||||
ref={studioInput}
|
ref={studioInput}
|
||||||
placeholder={intl.formatMessage({
|
placeholder={intl.formatMessage({
|
||||||
id: "studio_tagger.studio_names_separated_by_comma",
|
id: "studio_tagger.studio_names_or_stashids_separated_by_comma",
|
||||||
})}
|
})}
|
||||||
rows={6}
|
rows={6}
|
||||||
/>
|
/>
|
||||||
|
|
@ -715,14 +716,17 @@ export const StudioTagger: React.FC<ITaggerProps> = ({ studios }) => {
|
||||||
|
|
||||||
async function batchAdd(studioInput: string, createParent: boolean) {
|
async function batchAdd(studioInput: string, createParent: boolean) {
|
||||||
if (studioInput && selectedEndpoint) {
|
if (studioInput && selectedEndpoint) {
|
||||||
const names = studioInput
|
const inputs = studioInput
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((n) => n.trim())
|
.map((n) => n.trim())
|
||||||
.filter((n) => n.length > 0);
|
.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({
|
const ret = await mutateStashBoxBatchStudioTag({
|
||||||
names: names,
|
names: names.length > 0 ? names : undefined,
|
||||||
|
stash_ids: stashIds.length > 0 ? stashIds : undefined,
|
||||||
endpoint: selectedEndpointIndex,
|
endpoint: selectedEndpointIndex,
|
||||||
refresh: false,
|
refresh: false,
|
||||||
exclude_fields: config?.excludedStudioFields ?? [],
|
exclude_fields: config?.excludedStudioFields ?? [],
|
||||||
|
|
|
||||||
|
|
@ -1295,7 +1295,7 @@
|
||||||
"no_results_found": "No results found.",
|
"no_results_found": "No results found.",
|
||||||
"number_of_performers_will_be_processed": "{performer_count} performers will be processed",
|
"number_of_performers_will_be_processed": "{performer_count} performers will be processed",
|
||||||
"performer_already_tagged": "Performer already tagged",
|
"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_selection": "Performer selection",
|
||||||
"performer_successfully_tagged": "Performer successfully tagged:",
|
"performer_successfully_tagged": "Performer successfully tagged:",
|
||||||
"query_all_performers_in_the_database": "All performers in the database",
|
"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_job_queued": "Status: Tagging job queued",
|
||||||
"status_tagging_studios": "Status: Tagging studios",
|
"status_tagging_studios": "Status: Tagging studios",
|
||||||
"studio_already_tagged": "Studio already tagged",
|
"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_selection": "Studio selection",
|
||||||
"studio_successfully_tagged": "Studio successfully tagged",
|
"studio_successfully_tagged": "Studio successfully tagged",
|
||||||
"tag_status": "Tag Status",
|
"tag_status": "Tag Status",
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,29 @@ export const getStashIDs = (
|
||||||
endpoint,
|
endpoint,
|
||||||
updated_at,
|
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 };
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue