From 5a41001246262b2426ef1b176b5e3b9b84b03a37 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 17 Mar 2023 15:07:53 +1100 Subject: [PATCH] Fix batch performer tagging with multiple endpoints (#3548) * Set stash ids correctly during performer batch add * Refactor performer tagger dialogs --- internal/manager/manager_tasks.go | 8 + internal/manager/task_stash_box_tag.go | 184 ++++----- pkg/models/stash_ids.go | 12 + .../Tagger/performers/PerformerTagger.tsx | 364 +++++++++++------- 4 files changed, 332 insertions(+), 236 deletions(-) diff --git a/internal/manager/manager_tasks.go b/internal/manager/manager_tasks.go index de4807760..110e7eb10 100644 --- a/internal/manager/manager_tasks.go +++ b/internal/manager/manager_tasks.go @@ -336,6 +336,10 @@ func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, input StashBoxB for _, performerID := range input.PerformerIds { if id, err := strconv.Atoi(performerID); err == nil { performer, err := performerQuery.Find(ctx, id) + if err == nil { + err = performer.LoadStashIDs(ctx, performerQuery) + } + if err == nil { tasks = append(tasks, StashBoxPerformerTagTask{ performer: performer, @@ -382,6 +386,10 @@ func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, input StashBoxB } 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, StashBoxPerformerTagTask{ performer: performer, refresh: input.Refresh, diff --git a/internal/manager/task_stash_box_tag.go b/internal/manager/task_stash_box_tag.go index 246729733..886da242f 100644 --- a/internal/manager/task_stash_box_tag.go +++ b/internal/manager/task_stash_box_tag.go @@ -50,17 +50,10 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { if t.refresh { var performerID string - txnErr := txn.WithReadTxn(ctx, instance.Repository, func(ctx context.Context) error { - stashids, _ := instance.Repository.Performer.GetStashIDs(ctx, t.performer.ID) - for _, id := range stashids { - if id.Endpoint == t.box.Endpoint { - performerID = id.StashID - } + for _, id := range t.performer.StashIDs.List() { + if id.Endpoint == t.box.Endpoint { + performerID = id.StashID } - return nil - }) - if txnErr != nil { - logger.Warnf("error while executing read transaction: %v", err) } if performerID != "" { performer, err = client.FindStashBoxPerformerByID(ctx, performerID) @@ -87,80 +80,7 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { if performer != nil { if t.performer != nil { - partial := models.NewPerformerPartial() - - if performer.Aliases != nil && !excluded["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) - partial.Birthdate = models.NewOptionalDate(*value) - } - if performer.CareerLength != nil && !excluded["career_length"] { - partial.CareerLength = models.NewOptionalString(*performer.CareerLength) - } - if performer.Country != nil && !excluded["country"] { - partial.Country = models.NewOptionalString(*performer.Country) - } - if performer.Ethnicity != nil && !excluded["ethnicity"] { - partial.Ethnicity = models.NewOptionalString(*performer.Ethnicity) - } - if performer.EyeColor != nil && !excluded["eye_color"] { - partial.EyeColor = models.NewOptionalString(*performer.EyeColor) - } - if performer.FakeTits != nil && !excluded["fake_tits"] { - partial.FakeTits = models.NewOptionalString(*performer.FakeTits) - } - if performer.Gender != nil && !excluded["gender"] { - partial.Gender = models.NewOptionalString(*performer.Gender) - } - if performer.Height != nil && !excluded["height"] { - h, err := strconv.Atoi(*performer.Height) - if err == nil { - partial.Height = models.NewOptionalInt(h) - } - } - if performer.Weight != nil && !excluded["weight"] { - w, err := strconv.Atoi(*performer.Weight) - if err == nil { - partial.Weight = models.NewOptionalInt(w) - } - } - if performer.Instagram != nil && !excluded["instagram"] { - partial.Instagram = models.NewOptionalString(*performer.Instagram) - } - if performer.Measurements != nil && !excluded["measurements"] { - partial.Measurements = models.NewOptionalString(*performer.Measurements) - } - if excluded["name"] && performer.Name != nil { - partial.Name = models.NewOptionalString(*performer.Name) - } - if performer.Piercings != nil && !excluded["piercings"] { - partial.Piercings = models.NewOptionalString(*performer.Piercings) - } - if performer.Tattoos != nil && !excluded["tattoos"] { - partial.Tattoos = models.NewOptionalString(*performer.Tattoos) - } - if performer.Twitter != nil && !excluded["twitter"] { - partial.Twitter = models.NewOptionalString(*performer.Twitter) - } - if performer.URL != nil && !excluded["url"] { - partial.URL = models.NewOptionalString(*performer.URL) - } - if !t.refresh { - partial.StashIDs = &models.UpdateStashIDs{ - StashIDs: []models.StashID{ - { - Endpoint: t.box.Endpoint, - StashID: *performer.RemoteSiteID, - }, - }, - Mode: models.RelationshipUpdateModeSet, - } - } + partial := t.getPartial(performer, excluded) txnErr := txn.WithTxn(ctx, instance.Repository, func(ctx context.Context) error { r := instance.Repository @@ -168,12 +88,13 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { if len(performer.Images) > 0 && !excluded["image"] { image, err := utils.ReadImageFromURL(ctx, performer.Images[0]) - if err != nil { - return err - } - err = r.Performer.UpdateImage(ctx, t.performer.ID, image) - if err != nil { - return err + if err == nil { + err = r.Performer.UpdateImage(ctx, t.performer.ID, image) + if err != nil { + return err + } + } else { + logger.Warnf("Failed to read performer image: %v", err) } } @@ -187,7 +108,7 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { return err }) if txnErr != nil { - logger.Warnf("failure to execute partial update of performer: %v", err) + logger.Warnf("failure to execute partial update of performer: %v", txnErr) } } else if t.name != nil && performer.Name != nil { currentTime := time.Now() @@ -258,6 +179,87 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { } } +func (t *StashBoxPerformerTagTask) getPartial(performer *models.ScrapedPerformer, excluded map[string]bool) models.PerformerPartial { + partial := models.NewPerformerPartial() + + if performer.Aliases != nil && !excluded["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) + partial.Birthdate = models.NewOptionalDate(*value) + } + if performer.CareerLength != nil && !excluded["career_length"] { + partial.CareerLength = models.NewOptionalString(*performer.CareerLength) + } + if performer.Country != nil && !excluded["country"] { + partial.Country = models.NewOptionalString(*performer.Country) + } + if performer.Ethnicity != nil && !excluded["ethnicity"] { + partial.Ethnicity = models.NewOptionalString(*performer.Ethnicity) + } + if performer.EyeColor != nil && !excluded["eye_color"] { + partial.EyeColor = models.NewOptionalString(*performer.EyeColor) + } + if performer.FakeTits != nil && !excluded["fake_tits"] { + partial.FakeTits = models.NewOptionalString(*performer.FakeTits) + } + if performer.Gender != nil && !excluded["gender"] { + partial.Gender = models.NewOptionalString(*performer.Gender) + } + if performer.Height != nil && !excluded["height"] { + h, err := strconv.Atoi(*performer.Height) + if err == nil { + partial.Height = models.NewOptionalInt(h) + } + } + if performer.Weight != nil && !excluded["weight"] { + w, err := strconv.Atoi(*performer.Weight) + if err == nil { + partial.Weight = models.NewOptionalInt(w) + } + } + if performer.Instagram != nil && !excluded["instagram"] { + partial.Instagram = models.NewOptionalString(*performer.Instagram) + } + if performer.Measurements != nil && !excluded["measurements"] { + partial.Measurements = models.NewOptionalString(*performer.Measurements) + } + if excluded["name"] && performer.Name != nil { + partial.Name = models.NewOptionalString(*performer.Name) + } + if performer.Piercings != nil && !excluded["piercings"] { + partial.Piercings = models.NewOptionalString(*performer.Piercings) + } + if performer.Tattoos != nil && !excluded["tattoos"] { + partial.Tattoos = models.NewOptionalString(*performer.Tattoos) + } + if performer.Twitter != nil && !excluded["twitter"] { + partial.Twitter = models.NewOptionalString(*performer.Twitter) + } + if performer.URL != nil && !excluded["url"] { + partial.URL = models.NewOptionalString(*performer.URL) + } + if !t.refresh { + // #3547 - need to overwrite the stash id for the endpoint, but preserve + // existing stash ids for other endpoints + partial.StashIDs = &models.UpdateStashIDs{ + StashIDs: t.performer.StashIDs.List(), + Mode: models.RelationshipUpdateModeSet, + } + + partial.StashIDs.Set(models.StashID{ + Endpoint: t.box.Endpoint, + StashID: *performer.RemoteSiteID, + }) + } + + return partial +} + func getDate(val *string) *models.Date { if val == nil { return nil diff --git a/pkg/models/stash_ids.go b/pkg/models/stash_ids.go index 9c9cfc56a..fcc2bdec0 100644 --- a/pkg/models/stash_ids.go +++ b/pkg/models/stash_ids.go @@ -21,6 +21,18 @@ func (u *UpdateStashIDs) AddUnique(v StashID) { u.StashIDs = append(u.StashIDs, v) } +// Set sets or replaces the stash id for the endpoint in the provided value. +func (u *UpdateStashIDs) Set(v StashID) { + for i, vv := range u.StashIDs { + if vv.Endpoint == v.Endpoint { + u.StashIDs[i] = v + return + } + } + + u.StashIDs = append(u.StashIDs, v) +} + type StashIDCriterionInput struct { // If present, this value is treated as a predicate. // That is, it will filter based on stash_ids with the matching endpoint diff --git a/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx b/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx index e386bd2fb..97c3b329f 100755 --- a/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx +++ b/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { Button, Card, Form, InputGroup, ProgressBar } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; import { Link } from "react-router-dom"; @@ -30,6 +30,204 @@ type JobFragment = Pick< const CLASSNAME = "PerformerTagger"; +interface IPerformerBatchUpdateModal { + performers: GQL.PerformerDataFragment[]; + isIdle: boolean; + selectedEndpoint: { endpoint: string; index: number }; + onBatchUpdate: (queryAll: boolean, refresh: boolean) => void; + close: () => void; +} + +const PerformerBatchUpdateModal: React.FC = ({ + performers, + isIdle, + selectedEndpoint, + onBatchUpdate, + close, +}) => { + const intl = useIntl(); + + const [queryAll, setQueryAll] = useState(false); + + const [refresh, setRefresh] = useState(false); + const { data: allPerformers } = GQL.useFindPerformersQuery({ + variables: { + performer_filter: { + stash_id_endpoint: { + endpoint: selectedEndpoint.endpoint, + modifier: refresh + ? GQL.CriterionModifier.NotNull + : GQL.CriterionModifier.IsNull, + }, + }, + filter: { + per_page: 0, + }, + }, + }); + + const performerCount = useMemo(() => { + // get all stash ids for the selected endpoint + const filteredStashIDs = performers.map((p) => + p.stash_ids.filter((s) => s.endpoint === selectedEndpoint.endpoint) + ); + + return queryAll + ? allPerformers?.findPerformers.count + : filteredStashIDs.filter((s) => + // if refresh, then we filter out the performers without a stash id + // otherwise, we want untagged performers, filtering out those with a stash id + refresh ? s.length > 0 : s.length === 0 + ).length; + }, [queryAll, refresh, performers, allPerformers, selectedEndpoint.endpoint]); + + return ( + onBatchUpdate(queryAll, refresh), + }} + cancel={{ + text: intl.formatMessage({ id: "actions.cancel" }), + variant: "danger", + onClick: () => close(), + }} + disabled={!isIdle} + > + + +
+ +
+
+ } + defaultChecked + onChange={() => setQueryAll(false)} + /> + setQueryAll(true)} + /> +
+ + +
+ +
+
+ setRefresh(false)} + /> + + + + setRefresh(true)} + /> + + + +
+ + + +
+ ); +}; + +interface IPerformerBatchAddModal { + isIdle: boolean; + onBatchAdd: (input: string) => void; + close: () => void; +} + +const PerformerBatchAddModal: React.FC = ({ + isIdle, + onBatchAdd, + close, +}) => { + const intl = useIntl(); + + const performerInput = useRef(null); + + return ( + { + if (performerInput.current) { + onBatchAdd(performerInput.current.value); + } else { + close(); + } + }, + }} + cancel={{ + text: intl.formatMessage({ id: "actions.cancel" }), + variant: "danger", + onClick: () => close(), + }} + disabled={!isIdle} + > + + + + + + ); +}; + interface IPerformerTaggerListProps { performers: GQL.PerformerDataFragment[]; selectedEndpoint: { endpoint: string; index: number }; @@ -61,27 +259,9 @@ const PerformerTaggerList: React.FC = ({ Record> >({}); const [queries, setQueries] = useState>({}); - const [queryAll, setQueryAll] = useState(false); - const [refresh, setRefresh] = useState(false); - const { data: allPerformers } = GQL.useFindPerformersQuery({ - variables: { - performer_filter: { - stash_id: { - value: "", - modifier: refresh - ? GQL.CriterionModifier.NotNull - : GQL.CriterionModifier.IsNull, - }, - }, - filter: { - per_page: 0, - }, - }, - }); const [showBatchAdd, setShowBatchAdd] = useState(false); const [showBatchUpdate, setShowBatchUpdate] = useState(false); - const performerInput = useRef(null); const [error, setError] = useState< Record @@ -144,14 +324,12 @@ const PerformerTaggerList: React.FC = ({ .finally(() => setLoadingUpdate(undefined)); }; - async function handleBatchAdd() { - if (performerInput.current) { - onBatchAdd(performerInput.current.value); - } + async function handleBatchAdd(input: string) { + onBatchAdd(input); setShowBatchAdd(false); } - const handleBatchUpdate = () => { + const handleBatchUpdate = (queryAll: boolean, refresh: boolean) => { onBatchUpdate(!queryAll ? performers.map((p) => p.id) : undefined, refresh); setShowBatchUpdate(false); }; @@ -388,128 +566,24 @@ const PerformerTaggerList: React.FC = ({ return ( - setShowBatchUpdate(false), - }} - disabled={!isIdle} - > - - -
- -
-
- } - defaultChecked - onChange={() => setQueryAll(false)} - /> - setQueryAll(true)} - /> -
- - -
- -
-
- setRefresh(false)} - /> - - - - setRefresh(true)} - /> - - - -
- - - refresh ? p.stash_ids.length > 0 : p.stash_ids.length === 0 - ).length, - }} - /> - -
- setShowBatchAdd(false), - }} - disabled={!isIdle} - > - setShowBatchUpdate(false)} + isIdle={isIdle} + selectedEndpoint={selectedEndpoint} + performers={performers} + onBatchUpdate={handleBatchUpdate} /> - - - - + )} + + {showBatchAdd && ( + setShowBatchAdd(false)} + isIdle={isIdle} + onBatchAdd={handleBatchAdd} + /> + )} +