add default performer gender configuration option

- Introduced `defaultPerformerGender` input and output fields in GraphQL schema.
- Updated configuration resolver to handle default performer gender.
- Implemented logic to use default performer gender when creating new performers or scraping data.
- Added UI component for setting default performer gender in settings panel.
- Updated localization for the new default performer gender option.
This commit is contained in:
KennyG 2026-04-26 14:52:34 -04:00
parent 103181a6d2
commit 487c461a75
9 changed files with 80 additions and 8 deletions

View file

@ -420,6 +420,9 @@ input ConfigInterfaceInput {
"When true, disables all customizations (plugins, CSS, JavaScript, locales) for troubleshooting"
disableCustomizations: Boolean
"Default gender to apply when creating a performer without explicit gender"
defaultPerformerGender: GenderEnum
"Interface language"
language: String
@ -497,6 +500,9 @@ type ConfigInterfaceResult {
"When true, disables all customizations (plugins, CSS, JavaScript, locales) for troubleshooting"
disableCustomizations: Boolean
"Default gender to apply when creating a performer without explicit gender"
defaultPerformerGender: GenderEnum
"Interface language"
language: String

View file

@ -528,6 +528,7 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI
r.setConfigBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled)
r.setConfigBool(config.DisableCustomizations, input.DisableCustomizations)
r.setConfigString(config.DefaultPerformerGender, (*string)(input.DefaultPerformerGender))
if input.DisableDropdownCreate != nil {
ddc := input.DisableDropdownCreate

View file

@ -8,6 +8,7 @@ import (
"strconv"
"strings"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/performer"
"github.com/stashapp/stash/pkg/plugin/hook"
@ -21,6 +22,14 @@ const (
instagramURL = "https://instagram.com"
)
func performerGenderOrDefault(gender *models.GenderEnum) *models.GenderEnum {
if gender != nil {
return gender
}
return config.GetInstance().GetDefaultPerformerGender()
}
// used to refetch performer after hooks run
func (r *mutationResolver) getPerformer(ctx context.Context, id int) (ret *models.Performer, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
@ -44,7 +53,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
newPerformer.Name = strings.TrimSpace(input.Name)
newPerformer.Disambiguation = translator.string(input.Disambiguation)
newPerformer.Aliases = models.NewRelatedStrings(stringslice.UniqueExcludeFold(stringslice.TrimSpace(input.AliasList), newPerformer.Name))
newPerformer.Gender = input.Gender
newPerformer.Gender = performerGenderOrDefault(input.Gender)
newPerformer.Ethnicity = translator.string(input.Ethnicity)
newPerformer.Country = translator.string(input.Country)
newPerformer.EyeColor = translator.string(input.EyeColor)

View file

@ -162,6 +162,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
customLocales := config.GetCustomLocales()
customLocalesEnabled := config.GetCustomLocalesEnabled()
disableCustomizations := config.GetDisableCustomizations()
defaultPerformerGender := config.GetDefaultPerformerGender()
language := config.GetLanguage()
handyKey := config.GetHandyKey()
scriptOffset := config.GetFunscriptOffset()
@ -190,6 +191,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
CustomLocales: &customLocales,
CustomLocalesEnabled: &customLocalesEnabled,
DisableCustomizations: &disableCustomizations,
DefaultPerformerGender: defaultPerformerGender,
Language: &language,
ImageLightbox: &imageLightboxOptions,

View file

@ -206,6 +206,7 @@ const (
autostartVideoOnPlaySelectedDefault = true
ContinuePlaylistDefault = "continue_playlist_default"
ShowStudioAsText = "show_studio_as_text"
DefaultPerformerGender = "default_performer_gender"
CSSEnabled = "cssenabled"
JavascriptEnabled = "javascriptenabled"
CustomLocalesEnabled = "customlocalesenabled"
@ -1311,6 +1312,15 @@ func (i *Config) GetShowStudioAsText() bool {
return i.getBool(ShowStudioAsText)
}
func (i *Config) GetDefaultPerformerGender() *models.GenderEnum {
g := models.GenderEnum(i.getString(DefaultPerformerGender))
if !g.IsValid() {
return nil
}
return &g
}
func (i *Config) getSlideshowDelay() int {
// assume have lock

View file

@ -98,6 +98,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
customLocales
customLocalesEnabled
disableCustomizations
defaultPerformerGender
language
imageLightbox {
slideshowDelay

View file

@ -97,6 +97,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
const [scrapedPerformer, setScrapedPerformer] =
useState<GQL.ScrapedPerformer>();
const { configuration: stashConfig } = useConfigurationContext();
const defaultPerformerGender =
stashConfig?.interface.defaultPerformerGender ?? null;
const intl = useIntl();
@ -134,7 +136,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
name: performer.name ?? "",
disambiguation: performer.disambiguation ?? "",
alias_list: performer.alias_list ?? [],
gender: performer.gender ?? null,
gender: performer.gender ?? (isNew ? defaultPerformerGender : null),
birthdate: performer.birthdate ?? "",
death_date: performer.death_date ?? "",
country: performer.country ?? "",
@ -424,16 +426,23 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
const result = await queryScrapePerformer(selectedScraper.id, ret);
if (!result?.data?.scrapeSinglePerformer?.length) return;
const withDefaultGender = (
scrapedPerformerData: GQL.ScrapedPerformerDataFragment
) =>
!scrapedPerformerData.gender && defaultPerformerGender
? { ...scrapedPerformerData, gender: defaultPerformerGender }
: scrapedPerformerData;
const scrapedResult = withDefaultGender(
result.data.scrapeSinglePerformer[0]
);
// assume one result
// if this is a new performer, just dump the data
if (isNew) {
updatePerformerEditStateFromScraper(
result.data.scrapeSinglePerformer[0]
);
updatePerformerEditStateFromScraper(scrapedResult);
setScraper(undefined);
} else {
setScrapedPerformer(result.data.scrapeSinglePerformer[0]);
setScrapedPerformer(scrapedResult);
}
} catch (e) {
Toast.error(e);
@ -451,11 +460,19 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
return;
}
const scrapedResult =
!result.data.scrapePerformerURL.gender && defaultPerformerGender
? {
...result.data.scrapePerformerURL,
gender: defaultPerformerGender,
}
: result.data.scrapePerformerURL;
// if this is a new performer, just dump the data
if (isNew) {
updatePerformerEditStateFromScraper(result.data.scrapePerformerURL);
updatePerformerEditStateFromScraper(scrapedResult);
} else {
setScrapedPerformer(result.data.scrapePerformerURL);
setScrapedPerformer(scrapedResult);
}
} catch (e) {
Toast.error(e);

View file

@ -42,6 +42,7 @@ import {
defaultImageWallDirection,
defaultImageWallMargin,
} from "src/utils/imageWall";
import { genderList } from "src/utils/gender";
import { defaultMaxOptionsShown, defaultPreviewVolume } from "src/core/config";
import { PatchComponent } from "src/patch";
@ -525,6 +526,27 @@ export const SettingsInterfacePanel: React.FC = PatchComponent(
checked={ui.showLinksOnPerformerCard ?? undefined}
onChange={(v) => saveUI({ showLinksOnPerformerCard: v })}
/>
<SelectSetting
id="default-performer-gender"
headingID="config.ui.performer_list.options.default_gender.heading"
subHeadingID="config.ui.performer_list.options.default_gender.description"
value={iface.defaultPerformerGender ?? ""}
onChange={(v) =>
saveInterface({
defaultPerformerGender:
v === "" ? null : (v as GQL.GenderEnum),
})
}
>
<option value="">{intl.formatMessage({ id: "none" })}</option>
{genderList.map((gender) => (
<option key={gender} value={gender}>
{intl.formatMessage({
id: `gender_types.${gender.toLowerCase()}`,
})}
</option>
))}
</SelectSetting>
</SettingSection>
<SettingSection headingID="config.ui.image_wall.heading">

View file

@ -894,6 +894,10 @@
"performer_list": {
"heading": "Performer List",
"options": {
"default_gender": {
"description": "Sets the default gender for new performers when the scraper does not provide a gender.",
"heading": "Default performer gender"
},
"show_links_on_grid_card": {
"heading": "Display links on performer grid cards"
}