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" }