mirror of
https://github.com/stashapp/stash.git
synced 2026-02-26 09:12:23 +01:00
FR: Change Identify Settings to Use Gender Checkboxes (#6557)
This commit is contained in:
parent
7aa7276fa3
commit
26db935fad
11 changed files with 120 additions and 52 deletions
|
|
@ -215,7 +215,9 @@ input IdentifyMetadataOptionsInput {
|
|||
setCoverImage: Boolean
|
||||
setOrganized: Boolean
|
||||
"defaults to true if not provided"
|
||||
includeMalePerformers: Boolean
|
||||
includeMalePerformers: Boolean @deprecated(reason: "Use performerGenders")
|
||||
"Filter to only include performers with these genders. If not provided, all genders are included."
|
||||
performerGenders: [GenderEnum!]
|
||||
"defaults to true if not provided"
|
||||
skipMultipleMatches: Boolean
|
||||
"tag to tag skipped multiple matches with"
|
||||
|
|
@ -260,7 +262,9 @@ type IdentifyMetadataOptions {
|
|||
setCoverImage: Boolean
|
||||
setOrganized: Boolean
|
||||
"defaults to true if not provided"
|
||||
includeMalePerformers: Boolean
|
||||
includeMalePerformers: Boolean @deprecated(reason: "Use performerGenders")
|
||||
"Filter to only include performers with these genders. If not provided, all genders are included."
|
||||
performerGenders: [GenderEnum!]
|
||||
"defaults to true if not provided"
|
||||
skipMultipleMatches: Boolean
|
||||
"tag to tag skipped multiple matches with"
|
||||
|
|
|
|||
|
|
@ -147,6 +147,9 @@ func (t *SceneIdentifier) getOptions(source ScraperSource) MetadataOptions {
|
|||
if source.Options.IncludeMalePerformers != nil {
|
||||
options.IncludeMalePerformers = source.Options.IncludeMalePerformers
|
||||
}
|
||||
if source.Options.PerformerGenders != nil {
|
||||
options.PerformerGenders = source.Options.PerformerGenders
|
||||
}
|
||||
if source.Options.SkipMultipleMatches != nil {
|
||||
options.SkipMultipleMatches = source.Options.SkipMultipleMatches
|
||||
}
|
||||
|
|
@ -204,13 +207,23 @@ func (t *SceneIdentifier) getSceneUpdater(ctx context.Context, s *models.Scene,
|
|||
ret.Partial.StudioID = models.NewOptionalInt(*studioID)
|
||||
}
|
||||
|
||||
includeMalePerformers := true
|
||||
if options.IncludeMalePerformers != nil {
|
||||
includeMalePerformers = *options.IncludeMalePerformers
|
||||
// Determine allowed genders for performer filtering
|
||||
var allowedGenders []models.GenderEnum
|
||||
if options.PerformerGenders != nil {
|
||||
// New field takes precedence
|
||||
allowedGenders = options.PerformerGenders
|
||||
} else if options.IncludeMalePerformers != nil && !*options.IncludeMalePerformers {
|
||||
// Legacy: if includeMalePerformers is false, include all genders except male
|
||||
for _, g := range models.AllGenderEnum {
|
||||
if g != models.GenderEnumMale {
|
||||
allowedGenders = append(allowedGenders, g)
|
||||
}
|
||||
}
|
||||
}
|
||||
// nil allowedGenders means include all performers
|
||||
|
||||
addSkipSingleNamePerformerTag := false
|
||||
performerIDs, err := rel.performers(ctx, !includeMalePerformers)
|
||||
performerIDs, err := rel.performers(ctx, allowedGenders)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrSkipSingleNamePerformer) {
|
||||
addSkipSingleNamePerformerTag = true
|
||||
|
|
|
|||
|
|
@ -60,9 +60,15 @@ func TestSceneIdentifier_Identify(t *testing.T) {
|
|||
)
|
||||
|
||||
defaultOptions := &MetadataOptions{
|
||||
SetOrganized: &boolFalse,
|
||||
SetCoverImage: &boolFalse,
|
||||
IncludeMalePerformers: &boolFalse,
|
||||
SetOrganized: &boolFalse,
|
||||
SetCoverImage: &boolFalse,
|
||||
PerformerGenders: []models.GenderEnum{
|
||||
models.GenderEnumFemale,
|
||||
models.GenderEnumTransgenderFemale,
|
||||
models.GenderEnumTransgenderMale,
|
||||
models.GenderEnumIntersex,
|
||||
models.GenderEnumNonBinary,
|
||||
},
|
||||
SkipSingleNamePerformers: &boolFalse,
|
||||
}
|
||||
sources := []ScraperSource{
|
||||
|
|
@ -216,9 +222,15 @@ func TestSceneIdentifier_modifyScene(t *testing.T) {
|
|||
|
||||
boolFalse := false
|
||||
defaultOptions := &MetadataOptions{
|
||||
SetOrganized: &boolFalse,
|
||||
SetCoverImage: &boolFalse,
|
||||
IncludeMalePerformers: &boolFalse,
|
||||
SetOrganized: &boolFalse,
|
||||
SetCoverImage: &boolFalse,
|
||||
PerformerGenders: []models.GenderEnum{
|
||||
models.GenderEnumFemale,
|
||||
models.GenderEnumTransgenderFemale,
|
||||
models.GenderEnumTransgenderMale,
|
||||
models.GenderEnumIntersex,
|
||||
models.GenderEnumNonBinary,
|
||||
},
|
||||
SkipSingleNamePerformers: &boolFalse,
|
||||
}
|
||||
tr := &SceneIdentifier{
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
)
|
||||
|
||||
|
|
@ -32,7 +33,10 @@ type MetadataOptions struct {
|
|||
SetCoverImage *bool `json:"setCoverImage"`
|
||||
SetOrganized *bool `json:"setOrganized"`
|
||||
// defaults to true if not provided
|
||||
// Deprecated: use PerformerGenders instead
|
||||
IncludeMalePerformers *bool `json:"includeMalePerformers"`
|
||||
// Filter to only include performers with these genders. If not provided, all genders are included.
|
||||
PerformerGenders []models.GenderEnum `json:"performerGenders"`
|
||||
// defaults to true if not provided
|
||||
SkipMultipleMatches *bool `json:"skipMultipleMatches"`
|
||||
// ID of tag to tag skipped multiple matches with
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -69,7 +70,7 @@ func (g sceneRelationships) studio(ctx context.Context) (*int, error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (g sceneRelationships) performers(ctx context.Context, ignoreMale bool) ([]int, error) {
|
||||
func (g sceneRelationships) performers(ctx context.Context, allowedGenders []models.GenderEnum) ([]int, error) {
|
||||
fieldStrategy := g.fieldOptions["performers"]
|
||||
scraped := g.result.result.Performers
|
||||
|
||||
|
|
@ -97,8 +98,11 @@ func (g sceneRelationships) performers(ctx context.Context, ignoreMale bool) ([]
|
|||
singleNamePerformerSkipped := false
|
||||
|
||||
for _, p := range scraped {
|
||||
if ignoreMale && p.Gender != nil && strings.EqualFold(*p.Gender, models.GenderEnumMale.String()) {
|
||||
continue
|
||||
if allowedGenders != nil && p.Gender != nil {
|
||||
gender := models.GenderEnum(strings.ToUpper(*p.Gender))
|
||||
if !slices.Contains(allowedGenders, gender) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
performerID, err := getPerformerID(ctx, endpoint, g.performerCreator, p, createMissing, g.skipSingleNamePerformers)
|
||||
|
|
|
|||
|
|
@ -183,13 +183,13 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
|||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scene *models.Scene
|
||||
fieldOptions *FieldOptions
|
||||
scraped []*models.ScrapedPerformer
|
||||
ignoreMale bool
|
||||
want []int
|
||||
wantErr bool
|
||||
name string
|
||||
scene *models.Scene
|
||||
fieldOptions *FieldOptions
|
||||
scraped []*models.ScrapedPerformer
|
||||
allowedGenders []models.GenderEnum
|
||||
want []int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"ignore",
|
||||
|
|
@ -202,7 +202,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
|||
StoredID: &validStoredID,
|
||||
},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
|
|
@ -211,7 +211,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
|||
emptyScene,
|
||||
defaultOptions,
|
||||
[]*models.ScrapedPerformer{},
|
||||
false,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
|
|
@ -225,7 +225,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
|||
StoredID: &existingPerformerStr,
|
||||
},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
|
|
@ -239,7 +239,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
|||
StoredID: &validStoredID,
|
||||
},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
[]int{existingPerformerID, validStoredIDInt},
|
||||
false,
|
||||
},
|
||||
|
|
@ -254,7 +254,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
|||
Gender: &male,
|
||||
},
|
||||
},
|
||||
true,
|
||||
[]models.GenderEnum{models.GenderEnumFemale, models.GenderEnumTransgenderMale, models.GenderEnumTransgenderFemale, models.GenderEnumIntersex, models.GenderEnumNonBinary},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
|
|
@ -270,7 +270,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
|||
StoredID: &validStoredID,
|
||||
},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
[]int{validStoredIDInt},
|
||||
false,
|
||||
},
|
||||
|
|
@ -287,7 +287,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
|||
Gender: &female,
|
||||
},
|
||||
},
|
||||
true,
|
||||
[]models.GenderEnum{models.GenderEnumFemale, models.GenderEnumTransgenderMale, models.GenderEnumTransgenderFemale, models.GenderEnumIntersex, models.GenderEnumNonBinary},
|
||||
[]int{validStoredIDInt},
|
||||
false,
|
||||
},
|
||||
|
|
@ -304,7 +304,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
|||
StoredID: &invalidStoredID,
|
||||
},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
|
|
@ -319,7 +319,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
got, err := tr.performers(testCtx, tt.ignoreMale)
|
||||
got, err := tr.performers(testCtx, tt.allowedGenders)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("sceneRelationships.performers() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ fragment IdentifyMetadataOptionsData on IdentifyMetadataOptions {
|
|||
}
|
||||
setCoverImage
|
||||
setOrganized
|
||||
includeMalePerformers
|
||||
performerGenders
|
||||
skipMultipleMatches
|
||||
skipMultipleMatchTag
|
||||
skipSingleNamePerformers
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
|
|||
createMissing: true,
|
||||
},
|
||||
],
|
||||
includeMalePerformers: true,
|
||||
performerGenders: undefined,
|
||||
setCoverImage: true,
|
||||
setOrganized: false,
|
||||
skipMultipleMatches: true,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { IScraperSource } from "./constants";
|
|||
import { FieldOptionsList } from "./FieldOptions";
|
||||
import { ThreeStateBoolean } from "./ThreeStateBoolean";
|
||||
import { TagSelect } from "src/components/Shared/Select";
|
||||
import { genderList } from "src/utils/gender";
|
||||
|
||||
interface IOptionsEditor {
|
||||
options: GQL.IdentifyMetadataOptionsInput;
|
||||
|
|
@ -124,24 +125,52 @@ export const OptionsEditor: React.FC<IOptionsEditor> = ({
|
|||
)}
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-0">
|
||||
<ThreeStateBoolean
|
||||
id="include-male-performers"
|
||||
value={
|
||||
options.includeMalePerformers === null
|
||||
? undefined
|
||||
: options.includeMalePerformers
|
||||
}
|
||||
setValue={(v) =>
|
||||
setOptions({
|
||||
includeMalePerformers: v,
|
||||
})
|
||||
}
|
||||
label={intl.formatMessage({
|
||||
id: "config.tasks.identify.include_male_performers",
|
||||
})}
|
||||
defaultValue={defaultOptions?.includeMalePerformers ?? undefined}
|
||||
{...checkboxProps}
|
||||
/>
|
||||
<Form.Group controlId="performer-genders" className="mb-2">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="config.tasks.identify.performer_genders" />
|
||||
</Form.Label>
|
||||
{source && (
|
||||
<Form.Check
|
||||
id="performer-genders-use-default"
|
||||
label={intl.formatMessage({ id: "actions.use_default" })}
|
||||
checked={options.performerGenders == null}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.currentTarget.checked) {
|
||||
setOptions({ performerGenders: undefined });
|
||||
} else {
|
||||
setOptions({
|
||||
performerGenders:
|
||||
defaultOptions?.performerGenders ?? genderList.slice(),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(options.performerGenders != null || !source) &&
|
||||
genderList.map((gender) => {
|
||||
const performerGenders =
|
||||
options.performerGenders ?? genderList.slice();
|
||||
return (
|
||||
<Form.Check
|
||||
id={`identify-gender-${gender}`}
|
||||
key={gender}
|
||||
label={<FormattedMessage id={`gender_types.${gender}`} />}
|
||||
checked={performerGenders.includes(gender)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const isChecked = e.currentTarget.checked;
|
||||
setOptions({
|
||||
performerGenders: isChecked
|
||||
? [...performerGenders, gender]
|
||||
: performerGenders.filter((g) => g !== gender),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Form.Text className="text-muted">
|
||||
<FormattedMessage id="config.tasks.identify.performer_genders_desc" />
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<ThreeStateBoolean
|
||||
id="set-cover-image"
|
||||
value={
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ The following options can be configured:
|
|||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| Include male performers | If false, male performers will not be created or set on scenes. |
|
||||
| Performer genders | Filter which performer genders are included during identification. If no genders are selected, all performers are included regardless of gender. |
|
||||
| Set cover images | If false, scene cover images will not be modified. |
|
||||
| Set organized flag | If true, the organized flag is set to true when a scene is organized. |
|
||||
| Skip matches that have more than one result | If this is not enabled and more than one result is returned, one will be randomly chosen to match |
|
||||
|
|
|
|||
|
|
@ -547,6 +547,8 @@
|
|||
"identifying_from_paths": "Identifying scenes from the following paths",
|
||||
"identifying_scenes": "Identifying {num} {scene}",
|
||||
"include_male_performers": "Include male performers",
|
||||
"performer_genders": "Performer genders",
|
||||
"performer_genders_desc": "Performers with selected genders will be included during identification.",
|
||||
"set_cover_images": "Set cover images",
|
||||
"set_organized": "Set organised flag",
|
||||
"skip_multiple_matches": "Skip matches that have more than one result",
|
||||
|
|
|
|||
Loading…
Reference in a new issue