Add gender support for performer (#371)

Co-authored-by: HiddenPants255 <>
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
hiddenpants255 2020-04-01 05:36:38 +07:00 committed by GitHub
parent d886012d74
commit 494b794228
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 333 additions and 22 deletions

View file

@ -1,5 +1,6 @@
fragment SlimPerformerData on Performer {
id
name
gender
image_path
}

View file

@ -3,6 +3,7 @@ fragment PerformerData on Performer {
checksum
name
url
gender
twitter
instagram
birthdate

View file

@ -1,6 +1,7 @@
mutation PerformerCreate(
$name: String,
$url: String,
$gender: GenderEnum,
$birthdate: String,
$ethnicity: String,
$country: String,
@ -20,6 +21,7 @@ mutation PerformerCreate(
performerCreate(input: {
name: $name,
url: $url,
gender: $gender,
birthdate: $birthdate,
ethnicity: $ethnicity,
country: $country,
@ -44,6 +46,7 @@ mutation PerformerUpdate(
$id: ID!,
$name: String,
$url: String,
$gender: GenderEnum,
$birthdate: String,
$ethnicity: String,
$country: String,
@ -64,6 +67,7 @@ mutation PerformerUpdate(
id: $id,
name: $name,
url: $url,
gender: $gender,
birthdate: $birthdate,
ethnicity: $ethnicity,
country: $country,

View file

@ -46,6 +46,8 @@ input PerformerFilterType {
piercings: StringCriterionInput
"""Filter by aliases"""
aliases: StringCriterionInput
"""Filter by gender"""
gender: GenderCriterionInput
}
input SceneMarkerFilterType {
@ -115,3 +117,8 @@ input MultiCriterionInput {
value: [ID!]
modifier: CriterionModifier!
}
input GenderCriterionInput {
value: GenderEnum
modifier: CriterionModifier!
}

View file

@ -1,8 +1,17 @@
enum GenderEnum {
MALE
FEMALE
TRANSGENDER_MALE
TRANSGENDER_FEMALE
INTERSEX
}
type Performer {
id: ID!
checksum: String!
name: String
url: String
gender: GenderEnum
twitter: String
instagram: String
birthdate: String
@ -26,6 +35,7 @@ type Performer {
input PerformerCreateInput {
name: String
url: String
gender: GenderEnum
birthdate: String
ethnicity: String
country: String
@ -48,6 +58,7 @@ input PerformerUpdateInput {
id: ID!
name: String
url: String
gender: GenderEnum
birthdate: String
ethnicity: String
country: String

View file

@ -2,6 +2,7 @@ package api
import (
"context"
"github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/models"
)
@ -20,6 +21,19 @@ func (r *performerResolver) URL(ctx context.Context, obj *models.Performer) (*st
return nil, nil
}
func (r *performerResolver) Gender(ctx context.Context, obj *models.Performer) (*models.GenderEnum, error) {
var ret models.GenderEnum
if obj.Gender.Valid {
ret = models.GenderEnum(obj.Gender.String)
if ret.IsValid() {
return &ret, nil
}
}
return nil, nil
}
func (r *performerResolver) Twitter(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Twitter.Valid {
return &obj.Twitter.String, nil

View file

@ -42,6 +42,9 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
if input.URL != nil {
newPerformer.URL = sql.NullString{String: *input.URL, Valid: true}
}
if input.Gender != nil {
newPerformer.Gender = sql.NullString{String: input.Gender.String(), Valid: true}
}
if input.Birthdate != nil {
newPerformer.Birthdate = models.SQLiteDate{String: *input.Birthdate, Valid: true}
}
@ -128,6 +131,9 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
if input.URL != nil {
updatedPerformer.URL = sql.NullString{String: *input.URL, Valid: true}
}
if input.Gender != nil {
updatedPerformer.Gender = sql.NullString{String: input.Gender.String(), Valid: true}
}
if input.Birthdate != nil {
updatedPerformer.Birthdate = models.SQLiteDate{String: *input.Birthdate, Valid: true}
}

View file

@ -19,7 +19,7 @@ import (
var DB *sqlx.DB
var dbPath string
var appSchemaVersion uint = 4
var appSchemaVersion uint = 5
var databaseSchemaVersion uint
const sqlite3Driver = "sqlite3_regexp"

View file

@ -0,0 +1,89 @@
PRAGMA foreign_keys=off;
-- need to re-create the performers table without the added column.
-- also need re-create the performers_scenes table due to the foreign key
-- rename existing performers table
ALTER TABLE `performers` RENAME TO `performers_old`;
ALTER TABLE `performers_scenes` RENAME TO `performers_scenes_old`;
-- drop the indexes
DROP INDEX IF EXISTS `index_performers_on_name`;
DROP INDEX IF EXISTS `index_performers_on_checksum`;
DROP INDEX IF EXISTS `index_performers_scenes_on_scene_id`;
DROP INDEX IF EXISTS `index_performers_scenes_on_performer_id`;
-- recreate the tables
CREATE TABLE `performers` (
`id` integer not null primary key autoincrement,
`image` blob not null,
`checksum` varchar(255) not null,
`name` varchar(255),
`url` varchar(255),
`twitter` varchar(255),
`instagram` varchar(255),
`birthdate` date,
`ethnicity` varchar(255),
`country` varchar(255),
`eye_color` varchar(255),
`height` varchar(255),
`measurements` varchar(255),
`fake_tits` varchar(255),
`career_length` varchar(255),
`tattoos` varchar(255),
`piercings` varchar(255),
`aliases` varchar(255),
`favorite` boolean not null default '0',
`created_at` datetime not null,
`updated_at` datetime not null
);
CREATE TABLE `performers_scenes` (
`performer_id` integer,
`scene_id` integer,
foreign key(`performer_id`) references `performers`(`id`),
foreign key(`scene_id`) references `scenes`(`id`)
);
INSERT INTO `performers`
SELECT
`id`,
`image`,
`checksum`,
`name`,
`url`,
`twitter`,
`instagram`,
`birthdate`,
`ethnicity`,
`country`,
`eye_color`,
`height`,
`measurements`,
`fake_tits`,
`career_length`,
`tattoos`,
`piercings`,
`aliases`,
`favorite`,
`created_at`,
`updated_at`
FROM `performers_old`;
INSERT INTO `performers_scenes`
SELECT
`performer_id`,
`scene_id`
FROM `performers_scenes_old`;
DROP TABLE `performers_scenes_old`;
DROP TABLE `performers_old`;
-- re-create the indexes after removing the old tables
CREATE INDEX `index_performers_on_name` on `performers` (`name`);
CREATE INDEX `index_performers_on_checksum` on `performers` (`checksum`);
CREATE INDEX `index_performers_scenes_on_scene_id` on `performers_scenes` (`scene_id`);
CREATE INDEX `index_performers_scenes_on_performer_id` on `performers_scenes` (`performer_id`);
PRAGMA foreign_keys=on;

View file

@ -0,0 +1 @@
ALTER TABLE `performers` ADD COLUMN `gender` varchar(20);

View file

@ -3,12 +3,14 @@ package jsonschema
import (
"encoding/json"
"fmt"
"github.com/stashapp/stash/pkg/models"
"os"
"github.com/stashapp/stash/pkg/models"
)
type Performer struct {
Name string `json:"name,omitempty"`
Gender string `json:"gender,omitempty"`
URL string `json:"url,omitempty"`
Twitter string `json:"twitter,omitempty"`
Instagram string `json:"instagram,omitempty"`

View file

@ -238,6 +238,9 @@ func (t *ExportTask) ExportPerformers(ctx context.Context) {
if performer.Name.Valid {
newPerformerJSON.Name = performer.Name.String
}
if performer.Gender.Valid {
newPerformerJSON.Gender = performer.Gender.String
}
if performer.URL.Valid {
newPerformerJSON.URL = performer.URL.String
}

View file

@ -94,6 +94,9 @@ func (t *ImportTask) ImportPerformers(ctx context.Context) {
if performerJSON.Name != "" {
newPerformer.Name = sql.NullString{String: performerJSON.Name, Valid: true}
}
if performerJSON.Gender != "" {
newPerformer.Gender = sql.NullString{String: performerJSON.Gender, Valid: true}
}
if performerJSON.URL != "" {
newPerformer.URL = sql.NullString{String: performerJSON.URL, Valid: true}
}

View file

@ -9,6 +9,7 @@ type Performer struct {
Image []byte `db:"image" json:"image"`
Checksum string `db:"checksum" json:"checksum"`
Name sql.NullString `db:"name" json:"name"`
Gender sql.NullString `db:"gender" json:"gender"`
URL sql.NullString `db:"url" json:"url"`
Twitter sql.NullString `db:"twitter" json:"twitter"`
Instagram sql.NullString `db:"instagram" json:"instagram"`

View file

@ -18,10 +18,10 @@ func NewPerformerQueryBuilder() PerformerQueryBuilder {
func (qb *PerformerQueryBuilder) Create(newPerformer Performer, tx *sqlx.Tx) (*Performer, error) {
ensureTx(tx)
result, err := tx.NamedExec(
`INSERT INTO performers (image, checksum, name, url, twitter, instagram, birthdate, ethnicity, country,
`INSERT INTO performers (image, checksum, name, url, gender, twitter, instagram, birthdate, ethnicity, country,
eye_color, height, measurements, fake_tits, career_length, tattoos, piercings,
aliases, favorite, created_at, updated_at)
VALUES (:image, :checksum, :name, :url, :twitter, :instagram, :birthdate, :ethnicity, :country,
VALUES (:image, :checksum, :name, :url, :gender, :twitter, :instagram, :birthdate, :ethnicity, :country,
:eye_color, :height, :measurements, :fake_tits, :career_length, :tattoos, :piercings,
:aliases, :favorite, :created_at, :updated_at)
`,
@ -153,6 +153,11 @@ func (qb *PerformerQueryBuilder) Query(performerFilter *PerformerFilterType, fin
query.addArg(thisArgs...)
}
if gender := performerFilter.Gender; gender != nil {
query.addWhere("performers.gender = ?")
query.addArg(gender.Value.String())
}
handleStringCriterion(tableName+".ethnicity", performerFilter.Ethnicity, &query)
handleStringCriterion(tableName+".country", performerFilter.Country, &query)
handleStringCriterion(tableName+".eye_color", performerFilter.EyeColor, &query)

View file

@ -64,6 +64,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
const [url, setUrl] = useState<string>();
const [twitter, setTwitter] = useState<string>();
const [instagram, setInstagram] = useState<string>();
const [gender, setGender] = useState<string | undefined>(undefined);
// Network state
const [isLoading, setIsLoading] = useState(false);
@ -92,6 +93,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
setUrl(state.url ?? undefined);
setTwitter(state.twitter ?? undefined);
setInstagram(state.instagram ?? undefined);
setGender(StashService.genderToString((state as GQL.PerformerDataFragment).gender ?? undefined));
}
function updatePerformerEditStateFromScraper(
@ -153,7 +155,8 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
url,
twitter,
instagram,
image
image,
gender: StashService.stringToGender(gender)
};
if (!isNew) {
@ -397,6 +400,16 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
}
}
function renderGender() {
return TableUtils.renderHtmlSelect({
title: "Gender",
value: gender,
isEditing: !!isEditing,
onChange: (value: string) => setGender(value),
selectOptions: [""].concat(StashService.getGenderStrings()),
});
}
return (
<>
{renderDeleteAlert()}
@ -406,6 +419,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
<tbody>
{maybeRenderName()}
{maybeRenderAliases()}
{renderGender()}
{TableUtils.renderInputGroup({
title: "Birthdate",
value: birthdate,

View file

@ -681,6 +681,40 @@ export class StashService {
});
}
private static stringGenderMap = new Map<string, GQL.GenderEnum>(
[["Male", GQL.GenderEnum.Male],
["Female", GQL.GenderEnum.Female],
["Transgender Male", GQL.GenderEnum.TransgenderMale],
["Transgender Female", GQL.GenderEnum.TransgenderFemale],
["Intersex", GQL.GenderEnum.Intersex]]
);
public static genderToString(value?: GQL.GenderEnum) {
if (!value) {
return undefined;
}
const foundEntry = Array.from(StashService.stringGenderMap.entries()).find((e) => {
return e[1] === value;
});
if (foundEntry) {
return foundEntry[0];
}
}
public static stringToGender(value?: string) {
if (!value) {
return undefined;
}
return StashService.stringGenderMap.get(value);
}
public static getGenderStrings() {
return Array.from(StashService.stringGenderMap.keys());
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
}

View file

@ -29,7 +29,8 @@ export type CriterionType =
| "career_length"
| "tattoos"
| "piercings"
| "aliases";
| "aliases"
| "gender";
type Option = string | number | IOptionType;
export type CriterionValue = string | number | ILabeledId[];
@ -87,6 +88,8 @@ export abstract class Criterion {
return "Piercings";
case "aliases":
return "Aliases";
case "gender":
return "Gender";
}
}

View file

@ -0,0 +1,21 @@
import { CriterionModifier } from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import {
Criterion,
CriterionType,
ICriterionOption,
} from "./criterion";
export class GenderCriterion extends Criterion {
public type: CriterionType = "gender";
public parameterName: string = "gender";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: string[] = StashService.getGenderStrings();
public value: string = "";
}
export class GenderCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("gender");
public value: CriterionType = "gender";
}

View file

@ -16,6 +16,7 @@ import { RatingCriterion } from "./rating";
import { ResolutionCriterion } from "./resolution";
import { StudiosCriterion } from "./studios";
import { TagsCriterion } from "./tags";
import { GenderCriterion } from "./gender";
import { MoviesCriterion } from "./movies";
export function makeCriteria(type: CriterionType = "none") {
@ -60,6 +61,8 @@ export function makeCriteria(type: CriterionType = "none") {
];
return ret;
}
case "gender":
return new GenderCriterion();
case "ethnicity":
case "country":
case "eye_color":

View file

@ -46,6 +46,8 @@ import {
} from "./criteria/tags";
import { makeCriteria } from "./criteria/utils";
import { DisplayMode, FilterMode } from "./types";
import { GenderCriterionOption, GenderCriterion } from "./criteria/gender";
import { StashService } from "src/core/StashService";
import { MoviesCriterionOption, MoviesCriterion } from "./criteria/movies";
interface IQueryParameters {
@ -141,7 +143,8 @@ export class ListFilterModel {
this.criterionOptions = [
new NoneCriterionOption(),
new FavoriteCriterionOption()
new FavoriteCriterionOption(),
new GenderCriterionOption(),
];
this.criterionOptions = this.criterionOptions.concat(
@ -502,6 +505,11 @@ export class ListFilterModel {
result.aliases = { value: aCrit.value, modifier: aCrit.modifier };
break;
}
case "gender": {
const gCrit = criterion as GenderCriterion;
result.gender = { value: StashService.stringToGender(gCrit.value), modifier: gCrit.modifier };
break;
}
// no default
}
});

View file

@ -54,6 +54,7 @@ export const PerformerDetailsPanel: FunctionComponent<IPerformerDetailsProps> =
const [url, setUrl] = useState<string | undefined>(undefined);
const [twitter, setTwitter] = useState<string | undefined>(undefined);
const [instagram, setInstagram] = useState<string | undefined>(undefined);
const [gender, setGender] = useState<string | undefined>(undefined);
// Network state
const [isLoading, setIsLoading] = useState(false);
@ -80,6 +81,7 @@ export const PerformerDetailsPanel: FunctionComponent<IPerformerDetailsProps> =
setUrl(state.url);
setTwitter(state.twitter);
setInstagram(state.instagram);
setGender(StashService.genderToString((state as GQL.PerformerDataFragment).gender));
}
function updatePerformerEditStateFromScraper(state: Partial<GQL.ScrapedPerformerDataFragment | GQL.ScrapeFreeonesScrapeFreeones>) {
@ -147,6 +149,7 @@ export const PerformerDetailsPanel: FunctionComponent<IPerformerDetailsProps> =
twitter,
instagram,
image,
gender: StashService.stringToGender(gender)
};
if (!props.isNew) {
@ -373,6 +376,16 @@ export const PerformerDetailsPanel: FunctionComponent<IPerformerDetailsProps> =
}
}
function renderGender() {
return TableUtils.renderHtmlSelect({
title: "Gender",
value: gender,
isEditing: !!props.isEditing,
onChange: (value: string) => setGender(value),
selectOptions: [""].concat(StashService.getGenderStrings()),
});
}
const twitterPrefix = "https://twitter.com/";
const instagramPrefix = "https://www.instagram.com/";
@ -385,6 +398,7 @@ export const PerformerDetailsPanel: FunctionComponent<IPerformerDetailsProps> =
<tbody>
{maybeRenderName()}
{maybeRenderAliases()}
{renderGender()}
{TableUtils.renderInputGroup(
{title: "Birthdate (YYYY-MM-DD)", value: birthdate, isEditing: !!props.isEditing, onChange: setBirthdate})}
{renderEthnicity()}

View file

@ -580,6 +580,40 @@ export class StashService {
});
}
private static stringGenderMap = new Map<string, GQL.GenderEnum>(
[["Male", GQL.GenderEnum.Male],
["Female", GQL.GenderEnum.Female],
["Transgender Male", GQL.GenderEnum.TransgenderMale],
["Transgender Female", GQL.GenderEnum.TransgenderFemale],
["Intersex", GQL.GenderEnum.Intersex]]
);
public static genderToString(value?: GQL.GenderEnum) {
if (!value) {
return undefined;
}
const foundEntry = Array.from(StashService.stringGenderMap.entries()).find((e) => {
return e[1] === value;
});
if (foundEntry) {
return foundEntry[0];
}
}
public static stringToGender(value?: string) {
if (!value) {
return undefined;
}
return StashService.stringGenderMap.get(value);
}
public static getGenderStrings() {
return Array.from(StashService.stringGenderMap.keys());
}
public static nullToUndefined(value: any): any {
if (_.isPlainObject(value)) {
return _.mapValues(value, StashService.nullToUndefined);

View file

@ -28,7 +28,8 @@ export type CriterionType =
"career_length" |
"tattoos" |
"piercings" |
"aliases";
"aliases" |
"gender";
export abstract class Criterion<Option = any, Value = any> {
public static getLabel(type: CriterionType = "none"): string {
@ -58,6 +59,7 @@ export abstract class Criterion<Option = any, Value = any> {
case "tattoos": return "Tattoos";
case "piercings": return "Piercings";
case "aliases": return "Aliases";
case "gender": return "Gender";
}
}

View file

@ -0,0 +1,21 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import { StashService } from "../../../core/StashService";
import {
Criterion,
CriterionType,
ICriterionOption,
} from "./criterion";
export class GenderCriterion extends Criterion<string, string> {
public type: CriterionType = "gender";
public parameterName: string = "gender";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: string[] = StashService.getGenderStrings();
public value: string = "";
}
export class GenderCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("gender");
public value: CriterionType = "gender";
}

View file

@ -12,6 +12,7 @@ import { ResolutionCriterion } from "./resolution";
import { StudiosCriterion } from "./studios";
import { MoviesCriterion } from "./movies";
import { TagsCriterion } from "./tags";
import { GenderCriterion } from "./gender";
export function makeCriteria(type: CriterionType = "none") {
switch (type) {
@ -40,6 +41,7 @@ export function makeCriteria(type: CriterionType = "none") {
Criterion.getModifierOption(CriterionModifier.LessThan)
];
return ret;
case "gender": return new GenderCriterion();
case "ethnicity":
case "country":
case "eye_color":

View file

@ -23,6 +23,8 @@ import {
DisplayMode,
FilterMode,
} from "./types";
import { GenderCriterionOption, GenderCriterion } from "./criteria/gender";
import { StashService } from "../../core/StashService";
interface IQueryParameters {
sortby?: string;
@ -100,7 +102,8 @@ export class ListFilterModel {
this.criterionOptions = [
new NoneCriterionOption(),
new FavoriteCriterionOption()
new FavoriteCriterionOption(),
new GenderCriterionOption(),
];
this.criterionOptions = this.criterionOptions.concat(numberCriteria.concat(stringCriteria).map((c) => {
@ -419,6 +422,10 @@ export class ListFilterModel {
const aCrit = criterion as StringCriterion;
result.aliases = { value: aCrit.value, modifier: aCrit.modifier };
break;
case "gender":
const gCrit = criterion as GenderCriterion;
result.gender = { value: StashService.stringToGender(gCrit.value), modifier: gCrit.modifier };
break;
}
});
return result;