mirror of
https://github.com/stashapp/stash.git
synced 2026-05-09 05:05:29 +02:00
Merge f3ef04e4d7 into 46f72e5574
This commit is contained in:
commit
053e87bd07
12 changed files with 131 additions and 9 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
|
|||
customLocales
|
||||
customLocalesEnabled
|
||||
disableCustomizations
|
||||
defaultPerformerGender
|
||||
language
|
||||
imageLightbox {
|
||||
slideshowDelay
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue