From 487c461a75844fc6e95f2cf82278e007f0cbb6a9 Mon Sep 17 00:00:00 2001 From: KennyG Date: Sun, 26 Apr 2026 14:52:34 -0400 Subject: [PATCH 01/11] 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. --- graphql/schema/types/config.graphql | 6 ++++ internal/api/resolver_mutation_configure.go | 1 + internal/api/resolver_mutation_performer.go | 11 ++++++- internal/api/resolver_query_configuration.go | 2 ++ internal/manager/config/config.go | 10 ++++++ ui/v2.5/graphql/data/config.graphql | 1 + .../PerformerDetails/PerformerEditPanel.tsx | 31 ++++++++++++++----- .../SettingsInterfacePanel.tsx | 22 +++++++++++++ ui/v2.5/src/locales/en-GB.json | 4 +++ 9 files changed, 80 insertions(+), 8 deletions(-) diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 5ab7fdfea..e8f39c7a7 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -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 diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index 3df1c9114..241ce8434 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -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 diff --git a/internal/api/resolver_mutation_performer.go b/internal/api/resolver_mutation_performer.go index 6f88c54ca..c437e9c52 100644 --- a/internal/api/resolver_mutation_performer.go +++ b/internal/api/resolver_mutation_performer.go @@ -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) diff --git a/internal/api/resolver_query_configuration.go b/internal/api/resolver_query_configuration.go index cf2c0e3cc..b446562d2 100644 --- a/internal/api/resolver_query_configuration.go +++ b/internal/api/resolver_query_configuration.go @@ -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, diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index 19e263810..5b7276d79 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -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 diff --git a/ui/v2.5/graphql/data/config.graphql b/ui/v2.5/graphql/data/config.graphql index ba8215fe3..9bc4bf8a7 100644 --- a/ui/v2.5/graphql/data/config.graphql +++ b/ui/v2.5/graphql/data/config.graphql @@ -98,6 +98,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult { customLocales customLocalesEnabled disableCustomizations + defaultPerformerGender language imageLightbox { slideshowDelay diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index c94002412..cab445aac 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -97,6 +97,8 @@ export const PerformerEditPanel: React.FC = ({ const [scrapedPerformer, setScrapedPerformer] = useState(); const { configuration: stashConfig } = useConfigurationContext(); + const defaultPerformerGender = + stashConfig?.interface.defaultPerformerGender ?? null; const intl = useIntl(); @@ -134,7 +136,7 @@ export const PerformerEditPanel: React.FC = ({ 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 = ({ 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 = ({ 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); diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index 5b4e8c5de..feaa057da 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -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 })} /> + + saveInterface({ + defaultPerformerGender: + v === "" ? null : (v as GQL.GenderEnum), + }) + } + > + + {genderList.map((gender) => ( + + ))} + diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 4974c06ca..5fb0b837e 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -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" } From 1ebb498f11422081ac1c1e5f4114a2f8ca6956f0 Mon Sep 17 00:00:00 2001 From: KennyG Date: Sun, 26 Apr 2026 14:52:48 -0400 Subject: [PATCH 02/11] Fix localization for gender types in SettingsInterfacePanel --- .../Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index feaa057da..2e5def46c 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -542,7 +542,7 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( {genderList.map((gender) => ( ))} From 526687a0092a9c594a57ffdd31be6c9d87301c67 Mon Sep 17 00:00:00 2001 From: KennyG Date: Sun, 26 Apr 2026 15:01:04 -0400 Subject: [PATCH 03/11] Fix Prettier import wrapping in interface settings. Wrap the config import in SettingsInterfacePanel to satisfy format-check in CI. Made-with: Cursor --- .../SettingsInterfacePanel/SettingsInterfacePanel.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index 2e5def46c..3691b275f 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -43,7 +43,10 @@ import { defaultImageWallMargin, } from "src/utils/imageWall"; import { genderList } from "src/utils/gender"; -import { defaultMaxOptionsShown, defaultPreviewVolume } from "src/core/config"; +import { + defaultMaxOptionsShown, + defaultPreviewVolume, +} from "src/core/config"; import { PatchComponent } from "src/patch"; const allMenuItems = [ From 38a8ed81225e8be29ffdeae9fa2e1a633d1d6b04 Mon Sep 17 00:00:00 2001 From: KennyG Date: Sun, 26 Apr 2026 15:04:47 -0400 Subject: [PATCH 04/11] Apply Prettier formatting for settings gender selector. Update SettingsInterfacePanel formatting to satisfy UI format-check in CI. Made-with: Cursor --- .../SettingsInterfacePanel/SettingsInterfacePanel.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index 3691b275f..650b67ed3 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -43,10 +43,7 @@ import { defaultImageWallMargin, } from "src/utils/imageWall"; import { genderList } from "src/utils/gender"; -import { - defaultMaxOptionsShown, - defaultPreviewVolume, -} from "src/core/config"; +import { defaultMaxOptionsShown, defaultPreviewVolume } from "src/core/config"; import { PatchComponent } from "src/patch"; const allMenuItems = [ @@ -536,8 +533,7 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( value={iface.defaultPerformerGender ?? ""} onChange={(v) => saveInterface({ - defaultPerformerGender: - v === "" ? null : (v as GQL.GenderEnum), + defaultPerformerGender: v === "" ? null : (v as GQL.GenderEnum), }) } > From b23d83f990d6a5c996b8492045f8b8e0a490805d Mon Sep 17 00:00:00 2001 From: KennyG Date: Thu, 30 Apr 2026 19:47:45 -0400 Subject: [PATCH 05/11] Creating handler to clear setting. `setConfigStrong` is a no op when the input pointer is nul. When the UI sends`null`, for when the user sets the setting back to no default, the old setting is saved. Was able to confirm this was testing. Selecting "none" in the dropdown keeps the previous setting saved and I think this is the issue. Built a helper or having the UI send "" instead of null could be possible solutions. - Updated GraphQL schema to clarify the behavior of `defaultPerformerGender`, allowing an empty string to clear the setting. - Implemented `applyDefaultPerformerGenderInput` function in the resolver to handle updates and clearing of the default performer gender. - Adjusted the settings panel to directly pass the value for `defaultPerformerGender`, simplifying the change handling. Made-with: Cursor --- graphql/schema/types/config.graphql | 5 ++-- internal/api/resolver_mutation_configure.go | 25 ++++++++++++++++++- .../SettingsInterfacePanel.tsx | 6 +---- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index e8f39c7a7..18ee52b8b 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -420,8 +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 + """Default gender to apply when creating a performer without explicit gender. + Send an empty string to clear the setting.""" + defaultPerformerGender: String "Interface language" language: String diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index 241ce8434..c679b26fd 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -9,6 +9,7 @@ import ( "path/filepath" "regexp" "strconv" + "strings" "github.com/stashapp/stash/internal/manager" "github.com/stashapp/stash/internal/manager/config" @@ -62,6 +63,26 @@ func (r *mutationResolver) setConfigString(key string, value *string) { } } +// applyDefaultPerformerGenderInput updates or clears DefaultPerformerGender. +// Omit the field entirely (nil) to leave the stored value unchanged; send "" to clear it. +func (r *mutationResolver) applyDefaultPerformerGenderInput(value *string) error { + if value == nil { + return nil + } + c := config.GetInstance() + s := strings.TrimSpace(*value) + if s == "" { + c.SetString(config.DefaultPerformerGender, "") + return nil + } + g := models.GenderEnum(s) + if !g.IsValid() { + return fmt.Errorf("invalid default performer gender %q", s) + } + c.SetString(config.DefaultPerformerGender, s) + return nil +} + func (r *mutationResolver) setConfigBool(key string, value *bool) { c := config.GetInstance() if value != nil { @@ -528,7 +549,9 @@ 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 err := r.applyDefaultPerformerGenderInput(input.DefaultPerformerGender); err != nil { + return makeConfigInterfaceResult(), err + } if input.DisableDropdownCreate != nil { ddc := input.DisableDropdownCreate diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index 650b67ed3..0c3ff26cb 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -531,11 +531,7 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( 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), - }) - } + onChange={(v) => saveInterface({ defaultPerformerGender: v })} > {genderList.map((gender) => ( From 18eedfc724acb1270358bbffaed976ec7018ecad Mon Sep 17 00:00:00 2001 From: KennyG Date: Thu, 30 Apr 2026 19:49:02 -0400 Subject: [PATCH 06/11] Enhance default performer gender handling in configuration. - Added a check for empty string input in `GetDefaultPerformerGender` to return nil if no gender is specified. - Implemented logging for invalid default performer gender values to improve debugging. This change ensures that the configuration correctly handles cases where the default performer gender is not set, enhancing the robustness of the application. --- internal/manager/config/config.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index 5b7276d79..edcf167ca 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -1313,8 +1313,14 @@ func (i *Config) GetShowStudioAsText() bool { } func (i *Config) GetDefaultPerformerGender() *models.GenderEnum { - g := models.GenderEnum(i.getString(DefaultPerformerGender)) + s := i.getString(DefaultPerformerGender) + if s == "" { + return nil + } + + g := models.GenderEnum(s) if !g.IsValid() { + logger.Warnf("invalid default performer gender: %q", s) return nil } From 01eda04b6fcd16c964080ad8fa5b95b489571149 Mon Sep 17 00:00:00 2001 From: KennyG Date: Thu, 30 Apr 2026 19:51:33 -0400 Subject: [PATCH 07/11] Breakout default gender handling in Module. - Introduced a new utility function `withScrapedPerformerDefaultGender` to streamline the assignment of default gender when scraping performer data. - Replaced inline gender handling logic with the new utility function in both scraping results for new and existing performers, improving code readability and maintainability. This change enhances the clarity of gender assignment logic during performer data scraping. --- .../PerformerDetails/PerformerEditPanel.tsx | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index cab445aac..0eb7f11ff 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -58,6 +58,15 @@ const isScraper = ( scraper: GQL.Scraper | GQL.StashBox ): scraper is GQL.Scraper => (scraper as GQL.Scraper).id !== undefined; +function withScrapedPerformerDefaultGender< + T extends { gender?: GQL.GenderEnum | null }, +>(scraped: T, defaultGender: GQL.GenderEnum | null | undefined): T { + if (scraped.gender || !defaultGender) { + return scraped; + } + return { ...scraped, gender: defaultGender }; +} + interface IPerformerDetails { performer: Partial; isVisible: boolean; @@ -426,14 +435,9 @@ export const PerformerEditPanel: React.FC = ({ 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] + const scrapedResult = withScrapedPerformerDefaultGender( + result.data.scrapeSinglePerformer[0], + defaultPerformerGender ); // assume one result @@ -460,13 +464,10 @@ export const PerformerEditPanel: React.FC = ({ return; } - const scrapedResult = - !result.data.scrapePerformerURL.gender && defaultPerformerGender - ? { - ...result.data.scrapePerformerURL, - gender: defaultPerformerGender, - } - : result.data.scrapePerformerURL; + const scrapedResult = withScrapedPerformerDefaultGender( + result.data.scrapePerformerURL, + defaultPerformerGender + ); // if this is a new performer, just dump the data if (isNew) { From 849febc8d3fdb48ef826a0c10e40d36957478d86 Mon Sep 17 00:00:00 2001 From: KennyG Date: Fri, 1 May 2026 09:17:30 -0400 Subject: [PATCH 08/11] Update documentation for default performer gender handling. - Added a section in the Configuration.md to explain the new default performer gender setting and its impact on performer data scraping. - Updated ScraperDevelopment.md to clarify how the default performer gender is applied when gender is not specified in scraped results. - Enhanced Scraping.md with details on how the default performer gender setting influences performer scraping. These changes improve the clarity and usability of the documentation regarding gender handling in the application. --- ui/v2.5/src/docs/en/Manual/Configuration.md | 6 ++++++ ui/v2.5/src/docs/en/Manual/ScraperDevelopment.md | 2 +- ui/v2.5/src/docs/en/Manual/Scraping.md | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ui/v2.5/src/docs/en/Manual/Configuration.md b/ui/v2.5/src/docs/en/Manual/Configuration.md index 3a856b2d4..98cac4c8d 100644 --- a/ui/v2.5/src/docs/en/Manual/Configuration.md +++ b/ui/v2.5/src/docs/en/Manual/Configuration.md @@ -149,6 +149,12 @@ Some scrapers require a Chrome instance to function correctly. If left empty, st > **⚠️ Important:** As of Chrome 136 you need to specify `--user-data-dir` alongside `--remote-debugging-port`. Read more on their [official post](https://developer.chrome.com/blog/remote-debugging-port). +### Default Performer Gender + +**Default performer gender** is edited under `Settings → Interface` + +When set, performers will be assumed to be of the identified gender when no gender is supplied by scraper or other means. Also sets the gender in the create performer dialog to this gender by default. + ## Authentication By default, stash is not configured with any sort of password protection. To enable password protection, both `Username` and `Password` must be populated. Note that when entering a new username and password where none was set previously, the system will immediately request these credentials to log you in. diff --git a/ui/v2.5/src/docs/en/Manual/ScraperDevelopment.md b/ui/v2.5/src/docs/en/Manual/ScraperDevelopment.md index 858fb89a0..e031bd975 100644 --- a/ui/v2.5/src/docs/en/Manual/ScraperDevelopment.md +++ b/ui/v2.5/src/docs/en/Manual/ScraperDevelopment.md @@ -964,7 +964,7 @@ Weight > **⚠️ Important:** `Name` field is required. -> **⚠️ Note:** `Gender` must be one of `male`, `female`, `transgender_male`, `transgender_female`, `intersex`, `non_binary` (case insensitive). +> **⚠️ Note:** `Gender` must be one of `male`, `female`, `transgender_male`, `transgender_female`, `intersex`, `non_binary` (case insensitive). If gender is not set, gender will be observed by **default performer gender** (`Settings → Interface`) if set. ### Scene diff --git a/ui/v2.5/src/docs/en/Manual/Scraping.md b/ui/v2.5/src/docs/en/Manual/Scraping.md index 05f5c0984..9d2e27cdb 100644 --- a/ui/v2.5/src/docs/en/Manual/Scraping.md +++ b/ui/v2.5/src/docs/en/Manual/Scraping.md @@ -89,6 +89,10 @@ Click on the 🔍 button in the `edit` tab of an item. You will be presented wit Enter the URL in the `edit` tab of an Item. If a scraper is installed that supports that url, then a button will appear to scrape the metadata. +### Performer scraping and default gender + +For **performers**, if the scraped result does not supply a **gender** but **default performer gender** (`Settings → Interface`) is configured, Stash fills that gender when applying the scrape in the performer. Any gender returned by the scrape is kept. + ## Tagger view The Tagger view is accessed from the scenes page. It allows the user to run scrapers on all items on the current page. The Tagger presents the user with potential matches for an item from a selected stash-box instance or metadata source if supported. The user needs to select the correct metadata information to save. From 17fe7b1092a7a9aec9ad15cb18dbca9849d4d7ae Mon Sep 17 00:00:00 2001 From: KennyG Date: Fri, 1 May 2026 09:39:33 -0400 Subject: [PATCH 09/11] Refactor clear behavior in settings. - Updated GraphQL schema to change `defaultPerformerGender` type to `GenderEnum` and added a new field `clearDefaultPerformerGender` for clearing the setting. - Modified `applyDefaultPerformerGenderInput` function to accept the new input structure, allowing for clearer handling of gender updates and clearing. - Enhanced the `SettingsInterfacePanel` to implement the new logic for saving default performer gender, improving user experience when setting or clearing the default gender. These changes streamline the configuration process and improve the clarity of gender handling in the application. --- graphql/schema/types/config.graphql | 7 ++--- internal/api/resolver_mutation_configure.go | 27 +++++++++---------- .../SettingsInterfacePanel.tsx | 16 ++++++++++- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 18ee52b8b..687dcd8b4 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -420,9 +420,10 @@ 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. - Send an empty string to clear the setting.""" - defaultPerformerGender: String + "Default gender to apply when creating a performer without explicit gender" + defaultPerformerGender: GenderEnum + "Clear the stored default performer gender" + clearDefaultPerformerGender: Boolean "Interface language" language: String diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index c679b26fd..d677ec262 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -9,7 +9,6 @@ import ( "path/filepath" "regexp" "strconv" - "strings" "github.com/stashapp/stash/internal/manager" "github.com/stashapp/stash/internal/manager/config" @@ -64,22 +63,22 @@ func (r *mutationResolver) setConfigString(key string, value *string) { } // applyDefaultPerformerGenderInput updates or clears DefaultPerformerGender. -// Omit the field entirely (nil) to leave the stored value unchanged; send "" to clear it. -func (r *mutationResolver) applyDefaultPerformerGenderInput(value *string) error { +// Omit both fields to leave the stored value unchanged. +func (r *mutationResolver) applyDefaultPerformerGenderInput(value *models.GenderEnum, clear *bool) error { + if clear != nil && *clear { + if value != nil { + return fmt.Errorf("cannot set and clear default performer gender in the same request") + } + config.GetInstance().SetString(config.DefaultPerformerGender, "") + return nil + } if value == nil { return nil } - c := config.GetInstance() - s := strings.TrimSpace(*value) - if s == "" { - c.SetString(config.DefaultPerformerGender, "") - return nil + if !value.IsValid() { + return fmt.Errorf("invalid default performer gender %q", *value) } - g := models.GenderEnum(s) - if !g.IsValid() { - return fmt.Errorf("invalid default performer gender %q", s) - } - c.SetString(config.DefaultPerformerGender, s) + config.GetInstance().SetString(config.DefaultPerformerGender, value.String()) return nil } @@ -549,7 +548,7 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI r.setConfigBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled) r.setConfigBool(config.DisableCustomizations, input.DisableCustomizations) - if err := r.applyDefaultPerformerGenderInput(input.DefaultPerformerGender); err != nil { + if err := r.applyDefaultPerformerGenderInput(input.DefaultPerformerGender, input.ClearDefaultPerformerGender); err != nil { return makeConfigInterfaceResult(), err } diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index 0c3ff26cb..dc8613c6c 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -152,6 +152,20 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( }); } + function saveDefaultPerformerGender(v: string) { + saveInterface( + v === "" + ? { + clearDefaultPerformerGender: true, + defaultPerformerGender: null, + } + : { + clearDefaultPerformerGender: false, + defaultPerformerGender: v as GQL.GenderEnum, + } + ); + } + function validateLocaleString(v: string) { if (!v) return; try { @@ -531,7 +545,7 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( 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 })} + onChange={(v) => saveDefaultPerformerGender(v)} > {genderList.map((gender) => ( From 178124177e5b9ba190bdfed41c2803d4133881d3 Mon Sep 17 00:00:00 2001 From: KennyG Date: Fri, 1 May 2026 11:00:38 -0400 Subject: [PATCH 10/11] Prettier fix - Updated the type definition in the `withScrapedPerformerDefaultGender` utility function to improve clarity and maintainability. - Ensured consistency in handling optional gender values for scraped performers. This change enhances the readability of the code related to default gender assignment during performer data processing. --- .../Performers/PerformerDetails/PerformerEditPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index 0eb7f11ff..73485b012 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -59,7 +59,7 @@ const isScraper = ( ): scraper is GQL.Scraper => (scraper as GQL.Scraper).id !== undefined; function withScrapedPerformerDefaultGender< - T extends { gender?: GQL.GenderEnum | null }, + T extends { gender?: GQL.GenderEnum | null } >(scraped: T, defaultGender: GQL.GenderEnum | null | undefined): T { if (scraped.gender || !defaultGender) { return scraped; From f3ef04e4d75a82f01785cb33c83b3b491f070a6e Mon Sep 17 00:00:00 2001 From: KennyG Date: Fri, 1 May 2026 11:05:16 -0400 Subject: [PATCH 11/11] Fix CI regressions for default performer gender updates. Resolve the golangci-lint builtin name conflict and relax scraped performer helper typing so UI type-check passes when scraper gender is string-valued. --- internal/api/resolver_mutation_configure.go | 4 ++-- .../Performers/PerformerDetails/PerformerEditPanel.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index d677ec262..30f6289f8 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -64,8 +64,8 @@ func (r *mutationResolver) setConfigString(key string, value *string) { // applyDefaultPerformerGenderInput updates or clears DefaultPerformerGender. // Omit both fields to leave the stored value unchanged. -func (r *mutationResolver) applyDefaultPerformerGenderInput(value *models.GenderEnum, clear *bool) error { - if clear != nil && *clear { +func (r *mutationResolver) applyDefaultPerformerGenderInput(value *models.GenderEnum, shouldClear *bool) error { + if shouldClear != nil && *shouldClear { if value != nil { return fmt.Errorf("cannot set and clear default performer gender in the same request") } diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index 73485b012..1fe5643b6 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -59,12 +59,12 @@ const isScraper = ( ): scraper is GQL.Scraper => (scraper as GQL.Scraper).id !== undefined; function withScrapedPerformerDefaultGender< - T extends { gender?: GQL.GenderEnum | null } + T extends { gender?: string | null } >(scraped: T, defaultGender: GQL.GenderEnum | null | undefined): T { if (scraped.gender || !defaultGender) { return scraped; } - return { ...scraped, gender: defaultGender }; + return { ...scraped, gender: defaultGender } as T; } interface IPerformerDetails {