This commit is contained in:
Stash-KennyG 2026-05-05 00:44:11 -04:00 committed by GitHub
commit 053e87bd07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 131 additions and 9 deletions

View file

@ -420,6 +420,11 @@ 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
"Clear the stored default performer gender"
clearDefaultPerformerGender: Boolean
"Interface language"
language: String
@ -497,6 +502,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

@ -62,6 +62,26 @@ 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, shouldClear *bool) error {
if shouldClear != nil && *shouldClear {
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
}
if !value.IsValid() {
return fmt.Errorf("invalid default performer gender %q", *value)
}
config.GetInstance().SetString(config.DefaultPerformerGender, value.String())
return nil
}
func (r *mutationResolver) setConfigBool(key string, value *bool) {
c := config.GetInstance()
if value != nil {
@ -528,6 +548,9 @@ 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, input.ClearDefaultPerformerGender); err != nil {
return makeConfigInterfaceResult(), err
}
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,21 @@ func (i *Config) GetShowStudioAsText() bool {
return i.getBool(ShowStudioAsText)
}
func (i *Config) GetDefaultPerformerGender() *models.GenderEnum {
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
}
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

@ -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?: string | null }
>(scraped: T, defaultGender: GQL.GenderEnum | null | undefined): T {
if (scraped.gender || !defaultGender) {
return scraped;
}
return { ...scraped, gender: defaultGender } as T;
}
interface IPerformerDetails {
performer: Partial<GQL.PerformerDataFragment>;
isVisible: boolean;
@ -97,6 +106,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 +145,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 +435,18 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
const result = await queryScrapePerformer(selectedScraper.id, ret);
if (!result?.data?.scrapeSinglePerformer?.length) return;
const scrapedResult = withScrapedPerformerDefaultGender(
result.data.scrapeSinglePerformer[0],
defaultPerformerGender
);
// 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 +464,16 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
return;
}
const scrapedResult = withScrapedPerformerDefaultGender(
result.data.scrapePerformerURL,
defaultPerformerGender
);
// 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";
@ -151,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 {
@ -525,6 +540,22 @@ 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) => saveDefaultPerformerGender(v)}
>
<option value="">{intl.formatMessage({ id: "none" })}</option>
{genderList.map((gender) => (
<option key={gender} value={gender}>
{intl.formatMessage({
id: `gender_types.${gender}`,
})}
</option>
))}
</SelectSetting>
</SettingSection>
<SettingSection headingID="config.ui.image_wall.heading">

View file

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

View file

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

View file

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

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