Fix batch performer tagging with multiple endpoints (#3548)

* Set stash ids correctly during performer batch add
* Refactor performer tagger dialogs
This commit is contained in:
WithoutPants 2023-03-17 15:07:53 +11:00 committed by GitHub
parent 7cff71c35f
commit 5a41001246
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 332 additions and 236 deletions

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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<IPerformerBatchUpdateModal> = ({
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 (
<ModalComponent
show
icon={faTags}
header={intl.formatMessage({
id: "performer_tagger.update_performers",
})}
accept={{
text: intl.formatMessage({
id: "performer_tagger.update_performers",
}),
onClick: () => onBatchUpdate(queryAll, refresh),
}}
cancel={{
text: intl.formatMessage({ id: "actions.cancel" }),
variant: "danger",
onClick: () => close(),
}}
disabled={!isIdle}
>
<Form.Group>
<Form.Label>
<h6>
<FormattedMessage id="performer_tagger.performer_selection" />
</h6>
</Form.Label>
<Form.Check
id="query-page"
type="radio"
name="performer-query"
label={<FormattedMessage id="performer_tagger.current_page" />}
defaultChecked
onChange={() => setQueryAll(false)}
/>
<Form.Check
id="query-all"
type="radio"
name="performer-query"
label={intl.formatMessage({
id: "performer_tagger.query_all_performers_in_the_database",
})}
defaultChecked={false}
onChange={() => setQueryAll(true)}
/>
</Form.Group>
<Form.Group>
<Form.Label>
<h6>
<FormattedMessage id="performer_tagger.tag_status" />
</h6>
</Form.Label>
<Form.Check
id="untagged-performers"
type="radio"
name="performer-refresh"
label={intl.formatMessage({
id: "performer_tagger.untagged_performers",
})}
defaultChecked
onChange={() => setRefresh(false)}
/>
<Form.Text>
<FormattedMessage id="performer_tagger.updating_untagged_performers_description" />
</Form.Text>
<Form.Check
id="tagged-performers"
type="radio"
name="performer-refresh"
label={intl.formatMessage({
id: "performer_tagger.refresh_tagged_performers",
})}
defaultChecked={false}
onChange={() => setRefresh(true)}
/>
<Form.Text>
<FormattedMessage id="performer_tagger.refreshing_will_update_the_data" />
</Form.Text>
</Form.Group>
<b>
<FormattedMessage
id="performer_tagger.number_of_performers_will_be_processed"
values={{
performer_count: performerCount,
}}
/>
</b>
</ModalComponent>
);
};
interface IPerformerBatchAddModal {
isIdle: boolean;
onBatchAdd: (input: string) => void;
close: () => void;
}
const PerformerBatchAddModal: React.FC<IPerformerBatchAddModal> = ({
isIdle,
onBatchAdd,
close,
}) => {
const intl = useIntl();
const performerInput = useRef<HTMLTextAreaElement | null>(null);
return (
<ModalComponent
show
icon={faStar}
header={intl.formatMessage({
id: "performer_tagger.add_new_performers",
})}
accept={{
text: intl.formatMessage({
id: "performer_tagger.add_new_performers",
}),
onClick: () => {
if (performerInput.current) {
onBatchAdd(performerInput.current.value);
} else {
close();
}
},
}}
cancel={{
text: intl.formatMessage({ id: "actions.cancel" }),
variant: "danger",
onClick: () => close(),
}}
disabled={!isIdle}
>
<Form.Control
className="text-input"
as="textarea"
ref={performerInput}
placeholder={intl.formatMessage({
id: "performer_tagger.performer_names_separated_by_comma",
})}
rows={6}
/>
<Form.Text>
<FormattedMessage id="performer_tagger.any_names_entered_will_be_queried" />
</Form.Text>
</ModalComponent>
);
};
interface IPerformerTaggerListProps {
performers: GQL.PerformerDataFragment[];
selectedEndpoint: { endpoint: string; index: number };
@ -61,27 +259,9 @@ const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
Record<string, Partial<GQL.SlimPerformerDataFragment>>
>({});
const [queries, setQueries] = useState<Record<string, string>>({});
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<HTMLTextAreaElement | null>(null);
const [error, setError] = useState<
Record<string, { message?: string; details?: string } | undefined>
@ -144,14 +324,12 @@ const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
.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<IPerformerTaggerListProps> = ({
return (
<Card>
<ModalComponent
show={showBatchUpdate}
icon={faTags}
header={intl.formatMessage({
id: "performer_tagger.update_performers",
})}
accept={{
text: intl.formatMessage({
id: "performer_tagger.update_performers",
}),
onClick: handleBatchUpdate,
}}
cancel={{
text: intl.formatMessage({ id: "actions.cancel" }),
variant: "danger",
onClick: () => setShowBatchUpdate(false),
}}
disabled={!isIdle}
>
<Form.Group>
<Form.Label>
<h6>
<FormattedMessage id="performer_tagger.performer_selection" />
</h6>
</Form.Label>
<Form.Check
id="query-page"
type="radio"
name="performer-query"
label={<FormattedMessage id="performer_tagger.current_page" />}
defaultChecked
onChange={() => setQueryAll(false)}
/>
<Form.Check
id="query-all"
type="radio"
name="performer-query"
label={intl.formatMessage({
id: "performer_tagger.query_all_performers_in_the_database",
})}
defaultChecked={false}
onChange={() => setQueryAll(true)}
/>
</Form.Group>
<Form.Group>
<Form.Label>
<h6>
<FormattedMessage id="performer_tagger.tag_status" />
</h6>
</Form.Label>
<Form.Check
id="untagged-performers"
type="radio"
name="performer-refresh"
label={intl.formatMessage({
id: "performer_tagger.untagged_performers",
})}
defaultChecked
onChange={() => setRefresh(false)}
/>
<Form.Text>
<FormattedMessage id="performer_tagger.updating_untagged_performers_description" />
</Form.Text>
<Form.Check
id="tagged-performers"
type="radio"
name="performer-refresh"
label={intl.formatMessage({
id: "performer_tagger.refresh_tagged_performers",
})}
defaultChecked={false}
onChange={() => setRefresh(true)}
/>
<Form.Text>
<FormattedMessage id="performer_tagger.refreshing_will_update_the_data" />
</Form.Text>
</Form.Group>
<b>
<FormattedMessage
id="performer_tagger.number_of_performers_will_be_processed"
values={{
performer_count: queryAll
? allPerformers?.findPerformers.count
: performers.filter((p) =>
refresh ? p.stash_ids.length > 0 : p.stash_ids.length === 0
).length,
}}
/>
</b>
</ModalComponent>
<ModalComponent
show={showBatchAdd}
icon={faStar}
header={intl.formatMessage({
id: "performer_tagger.add_new_performers",
})}
accept={{
text: intl.formatMessage({
id: "performer_tagger.add_new_performers",
}),
onClick: handleBatchAdd,
}}
cancel={{
text: intl.formatMessage({ id: "actions.cancel" }),
variant: "danger",
onClick: () => setShowBatchAdd(false),
}}
disabled={!isIdle}
>
<Form.Control
className="text-input"
as="textarea"
ref={performerInput}
placeholder={intl.formatMessage({
id: "performer_tagger.performer_names_separated_by_comma",
})}
rows={6}
{showBatchUpdate && (
<PerformerBatchUpdateModal
close={() => setShowBatchUpdate(false)}
isIdle={isIdle}
selectedEndpoint={selectedEndpoint}
performers={performers}
onBatchUpdate={handleBatchUpdate}
/>
<Form.Text>
<FormattedMessage id="performer_tagger.any_names_entered_will_be_queried" />
</Form.Text>
</ModalComponent>
)}
{showBatchAdd && (
<PerformerBatchAddModal
close={() => setShowBatchAdd(false)}
isIdle={isIdle}
onBatchAdd={handleBatchAdd}
/>
)}
<div className="ml-auto mb-3">
<Button onClick={() => setShowBatchAdd(true)}>
<FormattedMessage id="performer_tagger.batch_add_performers" />