mirror of
https://github.com/stashapp/stash.git
synced 2026-04-12 10:00:49 +02:00
FR: Change Career Length to Career Start and Career End (#6449)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
parent
bede849fa6
commit
adaadee368
47 changed files with 1004 additions and 132 deletions
|
|
@ -140,4 +140,8 @@ models:
|
|||
fields:
|
||||
plugins:
|
||||
resolver: true
|
||||
Performer:
|
||||
fields:
|
||||
career_length:
|
||||
resolver: true
|
||||
|
||||
|
|
|
|||
|
|
@ -154,8 +154,13 @@ input PerformerFilterType {
|
|||
penis_length: FloatCriterionInput
|
||||
"Filter by ciricumcision"
|
||||
circumcised: CircumcisionCriterionInput
|
||||
"Filter by career length"
|
||||
"Deprecated: use career_start and career_end. This filter is non-functional."
|
||||
career_length: StringCriterionInput
|
||||
@deprecated(reason: "Use career_start and career_end")
|
||||
"Filter by career start year"
|
||||
career_start: IntCriterionInput
|
||||
"Filter by career end year"
|
||||
career_end: IntCriterionInput
|
||||
"Filter by tattoos"
|
||||
tattoos: StringCriterionInput
|
||||
"Filter by piercings"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ type Performer {
|
|||
fake_tits: String
|
||||
penis_length: Float
|
||||
circumcised: CircumisedEnum
|
||||
career_length: String
|
||||
career_length: String @deprecated(reason: "Use career_start and career_end")
|
||||
career_start: Int
|
||||
career_end: Int
|
||||
tattoos: String
|
||||
piercings: String
|
||||
alias_list: [String!]!
|
||||
|
|
@ -77,7 +79,9 @@ input PerformerCreateInput {
|
|||
fake_tits: String
|
||||
penis_length: Float
|
||||
circumcised: CircumisedEnum
|
||||
career_length: String
|
||||
career_length: String @deprecated(reason: "Use career_start and career_end")
|
||||
career_start: Int
|
||||
career_end: Int
|
||||
tattoos: String
|
||||
piercings: String
|
||||
"Duplicate aliases and those equal to name will be ignored (case-insensitive)"
|
||||
|
|
@ -116,7 +120,9 @@ input PerformerUpdateInput {
|
|||
fake_tits: String
|
||||
penis_length: Float
|
||||
circumcised: CircumisedEnum
|
||||
career_length: String
|
||||
career_length: String @deprecated(reason: "Use career_start and career_end")
|
||||
career_start: Int
|
||||
career_end: Int
|
||||
tattoos: String
|
||||
piercings: String
|
||||
"Duplicate aliases and those equal to name will be ignored (case-insensitive)"
|
||||
|
|
@ -160,7 +166,9 @@ input BulkPerformerUpdateInput {
|
|||
fake_tits: String
|
||||
penis_length: Float
|
||||
circumcised: CircumisedEnum
|
||||
career_length: String
|
||||
career_length: String @deprecated(reason: "Use career_start and career_end")
|
||||
career_start: Int
|
||||
career_end: Int
|
||||
tattoos: String
|
||||
piercings: String
|
||||
"Duplicate aliases and those equal to name will result in an error (case-insensitive)"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ type ScrapedPerformer {
|
|||
fake_tits: String
|
||||
penis_length: String
|
||||
circumcised: String
|
||||
career_length: String
|
||||
career_length: String @deprecated(reason: "Use career_start and career_end")
|
||||
career_start: Int
|
||||
career_end: Int
|
||||
tattoos: String
|
||||
piercings: String
|
||||
# aliases must be comma-delimited to be parsed correctly
|
||||
|
|
@ -54,7 +56,9 @@ input ScrapedPerformerInput {
|
|||
fake_tits: String
|
||||
penis_length: String
|
||||
circumcised: String
|
||||
career_length: String
|
||||
career_length: String @deprecated(reason: "Use career_start and career_end")
|
||||
career_start: Int
|
||||
career_end: Int
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *performerResolver) AliasList(ctx context.Context, obj *models.Performer) ([]string, error) {
|
||||
|
|
@ -109,6 +110,15 @@ func (r *performerResolver) HeightCm(ctx context.Context, obj *models.Performer)
|
|||
return obj.Height, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) CareerLength(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.CareerStart == nil && obj.CareerEnd == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ret := utils.FormatYearRange(obj.CareerStart, obj.CareerEnd)
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Birthdate(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Birthdate != nil {
|
||||
ret := obj.Birthdate.String()
|
||||
|
|
|
|||
|
|
@ -52,7 +52,17 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
|||
newPerformer.FakeTits = translator.string(input.FakeTits)
|
||||
newPerformer.PenisLength = input.PenisLength
|
||||
newPerformer.Circumcised = input.Circumcised
|
||||
newPerformer.CareerLength = translator.string(input.CareerLength)
|
||||
newPerformer.CareerStart = input.CareerStart
|
||||
newPerformer.CareerEnd = input.CareerEnd
|
||||
// if career_start/career_end not provided, parse deprecated career_length
|
||||
if newPerformer.CareerStart == nil && newPerformer.CareerEnd == nil && input.CareerLength != nil {
|
||||
start, end, err := utils.ParseYearRangeString(*input.CareerLength)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse career_length %q: %w", *input.CareerLength, err)
|
||||
}
|
||||
newPerformer.CareerStart = start
|
||||
newPerformer.CareerEnd = end
|
||||
}
|
||||
newPerformer.Tattoos = translator.string(input.Tattoos)
|
||||
newPerformer.Piercings = translator.string(input.Piercings)
|
||||
newPerformer.Favorite = translator.bool(input.Favorite)
|
||||
|
|
@ -261,7 +271,22 @@ func performerPartialFromInput(input models.PerformerUpdateInput, translator cha
|
|||
updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits")
|
||||
updatedPerformer.PenisLength = translator.optionalFloat64(input.PenisLength, "penis_length")
|
||||
updatedPerformer.Circumcised = translator.optionalString((*string)(input.Circumcised), "circumcised")
|
||||
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
|
||||
// prefer career_start/career_end over deprecated career_length
|
||||
if translator.hasField("career_start") || translator.hasField("career_end") {
|
||||
updatedPerformer.CareerStart = translator.optionalInt(input.CareerStart, "career_start")
|
||||
updatedPerformer.CareerEnd = translator.optionalInt(input.CareerEnd, "career_end")
|
||||
} else if translator.hasField("career_length") && input.CareerLength != nil {
|
||||
start, end, err := utils.ParseYearRangeString(*input.CareerLength)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse career_length %q: %w", *input.CareerLength, err)
|
||||
}
|
||||
if start != nil {
|
||||
updatedPerformer.CareerStart = models.NewOptionalInt(*start)
|
||||
}
|
||||
if end != nil {
|
||||
updatedPerformer.CareerEnd = models.NewOptionalInt(*end)
|
||||
}
|
||||
}
|
||||
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
|
||||
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
|
||||
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
|
|
@ -417,7 +442,22 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
|
|||
updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits")
|
||||
updatedPerformer.PenisLength = translator.optionalFloat64(input.PenisLength, "penis_length")
|
||||
updatedPerformer.Circumcised = translator.optionalString((*string)(input.Circumcised), "circumcised")
|
||||
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
|
||||
// prefer career_start/career_end over deprecated career_length
|
||||
if translator.hasField("career_start") || translator.hasField("career_end") {
|
||||
updatedPerformer.CareerStart = translator.optionalInt(input.CareerStart, "career_start")
|
||||
updatedPerformer.CareerEnd = translator.optionalInt(input.CareerEnd, "career_end")
|
||||
} else if translator.hasField("career_length") && input.CareerLength != nil {
|
||||
start, end, err := utils.ParseYearRangeString(*input.CareerLength)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse career_length %q: %w", *input.CareerLength, err)
|
||||
}
|
||||
if start != nil {
|
||||
updatedPerformer.CareerStart = models.NewOptionalInt(*start)
|
||||
}
|
||||
if end != nil {
|
||||
updatedPerformer.CareerEnd = models.NewOptionalInt(*end)
|
||||
}
|
||||
}
|
||||
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
|
||||
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ type Performer struct {
|
|||
FakeTits string `json:"fake_tits,omitempty"`
|
||||
PenisLength float64 `json:"penis_length,omitempty"`
|
||||
Circumcised string `json:"circumcised,omitempty"`
|
||||
CareerLength string `json:"career_length,omitempty"`
|
||||
CareerLength string `json:"career_length,omitempty"` // deprecated - for import only
|
||||
CareerStart *int `json:"career_start,omitempty"`
|
||||
CareerEnd *int `json:"career_end,omitempty"`
|
||||
Tattoos string `json:"tattoos,omitempty"`
|
||||
Piercings string `json:"piercings,omitempty"`
|
||||
Aliases StringOrStringList `json:"aliases,omitempty"`
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ type Performer struct {
|
|||
FakeTits string `json:"fake_tits"`
|
||||
PenisLength *float64 `json:"penis_length"`
|
||||
Circumcised *CircumisedEnum `json:"circumcised"`
|
||||
CareerLength string `json:"career_length"`
|
||||
CareerStart *int `json:"career_start"`
|
||||
CareerEnd *int `json:"career_end"`
|
||||
Tattoos string `json:"tattoos"`
|
||||
Piercings string `json:"piercings"`
|
||||
Favorite bool `json:"favorite"`
|
||||
|
|
@ -75,7 +76,8 @@ type PerformerPartial struct {
|
|||
FakeTits OptionalString
|
||||
PenisLength OptionalFloat64
|
||||
Circumcised OptionalString
|
||||
CareerLength OptionalString
|
||||
CareerStart OptionalInt
|
||||
CareerEnd OptionalInt
|
||||
Tattoos OptionalString
|
||||
Piercings OptionalString
|
||||
Favorite OptionalBool
|
||||
|
|
|
|||
|
|
@ -176,7 +176,9 @@ type ScrapedPerformer struct {
|
|||
FakeTits *string `json:"fake_tits"`
|
||||
PenisLength *string `json:"penis_length"`
|
||||
Circumcised *string `json:"circumcised"`
|
||||
CareerLength *string `json:"career_length"`
|
||||
CareerLength *string `json:"career_length"` // deprecated: use CareerStart/CareerEnd
|
||||
CareerStart *int `json:"career_start"`
|
||||
CareerEnd *int `json:"career_end"`
|
||||
Tattoos *string `json:"tattoos"`
|
||||
Piercings *string `json:"piercings"`
|
||||
Aliases *string `json:"aliases"`
|
||||
|
|
@ -219,8 +221,16 @@ func (p *ScrapedPerformer) ToPerformer(endpoint string, excluded map[string]bool
|
|||
ret.DeathDate = &date
|
||||
}
|
||||
}
|
||||
if p.CareerLength != nil && !excluded["career_length"] {
|
||||
ret.CareerLength = *p.CareerLength
|
||||
|
||||
// assume that career length is _not_ populated in favour of start/end
|
||||
|
||||
if p.CareerStart != nil && !excluded["career_start"] {
|
||||
cs := *p.CareerStart
|
||||
ret.CareerStart = &cs
|
||||
}
|
||||
if p.CareerEnd != nil && !excluded["career_end"] {
|
||||
ce := *p.CareerEnd
|
||||
ret.CareerEnd = &ce
|
||||
}
|
||||
if p.Country != nil && !excluded["country"] {
|
||||
ret.Country = *p.Country
|
||||
|
|
@ -356,7 +366,16 @@ func (p *ScrapedPerformer) ToPartial(endpoint string, excluded map[string]bool,
|
|||
}
|
||||
}
|
||||
if p.CareerLength != nil && !excluded["career_length"] {
|
||||
ret.CareerLength = NewOptionalString(*p.CareerLength)
|
||||
// parse career_length into career_start/career_end
|
||||
start, end, err := utils.ParseYearRangeString(*p.CareerLength)
|
||||
if err == nil {
|
||||
if start != nil {
|
||||
ret.CareerStart = NewOptionalInt(*start)
|
||||
}
|
||||
if end != nil {
|
||||
ret.CareerEnd = NewOptionalInt(*end)
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.Country != nil && !excluded["country"] {
|
||||
ret.Country = NewOptionalString(*p.Country)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func intPtr(i int) *int { return &i }
|
||||
|
||||
func Test_scrapedToStudioInput(t *testing.T) {
|
||||
const name = "name"
|
||||
url := "url"
|
||||
|
|
@ -124,9 +126,10 @@ func Test_scrapedToPerformerInput(t *testing.T) {
|
|||
endpoint := "endpoint"
|
||||
remoteSiteID := "remoteSiteID"
|
||||
|
||||
var stringValues []string
|
||||
for i := 0; i < 20; i++ {
|
||||
stringValues = append(stringValues, strconv.Itoa(i))
|
||||
const nValues = 19
|
||||
stringValues := make([]string, nValues)
|
||||
for i := 0; i < nValues; i++ {
|
||||
stringValues[i] = strconv.Itoa(i)
|
||||
}
|
||||
|
||||
upTo := 0
|
||||
|
|
@ -183,7 +186,8 @@ func Test_scrapedToPerformerInput(t *testing.T) {
|
|||
Weight: nextVal(),
|
||||
Measurements: nextVal(),
|
||||
FakeTits: nextVal(),
|
||||
CareerLength: nextVal(),
|
||||
CareerStart: intPtr(2005),
|
||||
CareerEnd: intPtr(2015),
|
||||
Tattoos: nextVal(),
|
||||
Piercings: nextVal(),
|
||||
Aliases: nextVal(),
|
||||
|
|
@ -208,8 +212,9 @@ func Test_scrapedToPerformerInput(t *testing.T) {
|
|||
Weight: nextIntVal(),
|
||||
Measurements: *nextVal(),
|
||||
FakeTits: *nextVal(),
|
||||
CareerLength: *nextVal(),
|
||||
Tattoos: *nextVal(),
|
||||
CareerStart: intPtr(2005),
|
||||
CareerEnd: intPtr(2015),
|
||||
Tattoos: *nextVal(), // skip CareerLength counter slot
|
||||
Piercings: *nextVal(),
|
||||
Aliases: NewRelatedStrings([]string{*nextVal()}),
|
||||
URLs: NewRelatedStrings([]string{*nextVal(), *nextVal(), *nextVal()}),
|
||||
|
|
|
|||
|
|
@ -137,7 +137,11 @@ type PerformerFilterType struct {
|
|||
// Filter by circumcision
|
||||
Circumcised *CircumcisionCriterionInput `json:"circumcised"`
|
||||
// Filter by career length
|
||||
CareerLength *StringCriterionInput `json:"career_length"`
|
||||
CareerLength *StringCriterionInput `json:"career_length"` // deprecated
|
||||
// Filter by career start year
|
||||
CareerStart *IntCriterionInput `json:"career_start"`
|
||||
// Filter by career end year
|
||||
CareerEnd *IntCriterionInput `json:"career_end"`
|
||||
// Filter by tattoos
|
||||
Tattoos *StringCriterionInput `json:"tattoos"`
|
||||
// Filter by piercings
|
||||
|
|
@ -224,6 +228,8 @@ type PerformerCreateInput struct {
|
|||
PenisLength *float64 `json:"penis_length"`
|
||||
Circumcised *CircumisedEnum `json:"circumcised"`
|
||||
CareerLength *string `json:"career_length"`
|
||||
CareerStart *int `json:"career_start"`
|
||||
CareerEnd *int `json:"career_end"`
|
||||
Tattoos *string `json:"tattoos"`
|
||||
Piercings *string `json:"piercings"`
|
||||
Aliases *string `json:"aliases"`
|
||||
|
|
@ -263,6 +269,8 @@ type PerformerUpdateInput struct {
|
|||
PenisLength *float64 `json:"penis_length"`
|
||||
Circumcised *CircumisedEnum `json:"circumcised"`
|
||||
CareerLength *string `json:"career_length"`
|
||||
CareerStart *int `json:"career_start"`
|
||||
CareerEnd *int `json:"career_end"`
|
||||
Tattoos *string `json:"tattoos"`
|
||||
Piercings *string `json:"piercings"`
|
||||
Aliases *string `json:"aliases"`
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ func ToJSON(ctx context.Context, reader ImageAliasStashIDGetter, performer *mode
|
|||
EyeColor: performer.EyeColor,
|
||||
Measurements: performer.Measurements,
|
||||
FakeTits: performer.FakeTits,
|
||||
CareerLength: performer.CareerLength,
|
||||
Tattoos: performer.Tattoos,
|
||||
Piercings: performer.Piercings,
|
||||
Favorite: performer.Favorite,
|
||||
|
|
@ -71,6 +70,13 @@ func ToJSON(ctx context.Context, reader ImageAliasStashIDGetter, performer *mode
|
|||
newPerformerJSON.PenisLength = *performer.PenisLength
|
||||
}
|
||||
|
||||
if performer.CareerStart != nil {
|
||||
newPerformerJSON.CareerStart = performer.CareerStart
|
||||
}
|
||||
if performer.CareerEnd != nil {
|
||||
newPerformerJSON.CareerEnd = performer.CareerEnd
|
||||
}
|
||||
|
||||
if err := performer.LoadAliases(ctx, reader); err != nil {
|
||||
return nil, fmt.Errorf("loading performer aliases: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ const (
|
|||
performerName = "testPerformer"
|
||||
disambiguation = "disambiguation"
|
||||
url = "url"
|
||||
careerLength = "careerLength"
|
||||
country = "country"
|
||||
ethnicity = "ethnicity"
|
||||
eyeColor = "eyeColor"
|
||||
|
|
@ -49,6 +48,8 @@ var (
|
|||
rating = 5
|
||||
height = 123
|
||||
weight = 60
|
||||
careerStart = 2005
|
||||
careerEnd = 2015
|
||||
penisLength = 1.23
|
||||
circumcisedEnum = models.CircumisedEnumCut
|
||||
circumcised = circumcisedEnum.String()
|
||||
|
|
@ -87,7 +88,8 @@ func createFullPerformer(id int, name string) *models.Performer {
|
|||
URLs: models.NewRelatedStrings([]string{url, twitter, instagram}),
|
||||
Aliases: models.NewRelatedStrings(aliases),
|
||||
Birthdate: &birthDate,
|
||||
CareerLength: careerLength,
|
||||
CareerStart: &careerStart,
|
||||
CareerEnd: &careerEnd,
|
||||
Country: country,
|
||||
Ethnicity: ethnicity,
|
||||
EyeColor: eyeColor,
|
||||
|
|
@ -132,7 +134,8 @@ func createFullJSONPerformer(name string, image string, withCustomFields bool) *
|
|||
URLs: []string{url, twitter, instagram},
|
||||
Aliases: aliases,
|
||||
Birthdate: birthDate.String(),
|
||||
CareerLength: careerLength,
|
||||
CareerStart: &careerStart,
|
||||
CareerEnd: &careerEnd,
|
||||
Country: country,
|
||||
Ethnicity: ethnicity,
|
||||
EyeColor: eyeColor,
|
||||
|
|
|
|||
|
|
@ -32,14 +32,17 @@ type Importer struct {
|
|||
}
|
||||
|
||||
func (i *Importer) PreImport(ctx context.Context) error {
|
||||
i.performer = performerJSONToPerformer(i.Input)
|
||||
var err error
|
||||
i.performer, err = performerJSONToPerformer(i.Input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.customFields = i.Input.CustomFields
|
||||
|
||||
if err := i.populateTags(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
if len(i.Input.Image) > 0 {
|
||||
i.imageData, err = utils.ProcessBase64Image(i.Input.Image)
|
||||
if err != nil {
|
||||
|
|
@ -196,7 +199,7 @@ func (i *Importer) Update(ctx context.Context, id int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Performer {
|
||||
func performerJSONToPerformer(performerJSON jsonschema.Performer) (models.Performer, error) {
|
||||
newPerformer := models.Performer{
|
||||
Name: performerJSON.Name,
|
||||
Disambiguation: performerJSON.Disambiguation,
|
||||
|
|
@ -205,7 +208,6 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform
|
|||
EyeColor: performerJSON.EyeColor,
|
||||
Measurements: performerJSON.Measurements,
|
||||
FakeTits: performerJSON.FakeTits,
|
||||
CareerLength: performerJSON.CareerLength,
|
||||
Tattoos: performerJSON.Tattoos,
|
||||
Piercings: performerJSON.Piercings,
|
||||
Aliases: models.NewRelatedStrings(performerJSON.Aliases),
|
||||
|
|
@ -282,5 +284,18 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform
|
|||
}
|
||||
}
|
||||
|
||||
return newPerformer
|
||||
// prefer explicit career_start/career_end, fall back to parsing legacy career_length
|
||||
if performerJSON.CareerStart != nil || performerJSON.CareerEnd != nil {
|
||||
newPerformer.CareerStart = performerJSON.CareerStart
|
||||
newPerformer.CareerEnd = performerJSON.CareerEnd
|
||||
} else if performerJSON.CareerLength != "" {
|
||||
start, end, err := utils.ParseYearRangeString(performerJSON.CareerLength)
|
||||
if err != nil {
|
||||
return models.Performer{}, fmt.Errorf("invalid career_length %q: %w", performerJSON.CareerLength, err)
|
||||
}
|
||||
newPerformer.CareerStart = start
|
||||
newPerformer.CareerEnd = end
|
||||
}
|
||||
|
||||
return newPerformer, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,3 +315,86 @@ func TestUpdate(t *testing.T) {
|
|||
|
||||
db.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestImportCareerFields(t *testing.T) {
|
||||
startYear := 2005
|
||||
endYear := 2015
|
||||
|
||||
// explicit career_start/career_end should be used directly
|
||||
t.Run("explicit fields", func(t *testing.T) {
|
||||
input := jsonschema.Performer{
|
||||
Name: "test",
|
||||
CareerStart: &startYear,
|
||||
CareerEnd: &endYear,
|
||||
}
|
||||
|
||||
p, err := performerJSONToPerformer(input)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, &startYear, p.CareerStart)
|
||||
assert.Equal(t, &endYear, p.CareerEnd)
|
||||
})
|
||||
|
||||
// explicit fields take priority over legacy career_length
|
||||
t.Run("explicit fields override legacy", func(t *testing.T) {
|
||||
input := jsonschema.Performer{
|
||||
Name: "test",
|
||||
CareerStart: &startYear,
|
||||
CareerEnd: &endYear,
|
||||
CareerLength: "1990 - 1995",
|
||||
}
|
||||
|
||||
p, err := performerJSONToPerformer(input)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, &startYear, p.CareerStart)
|
||||
assert.Equal(t, &endYear, p.CareerEnd)
|
||||
})
|
||||
|
||||
// legacy career_length should be parsed when explicit fields are absent
|
||||
t.Run("legacy career_length fallback", func(t *testing.T) {
|
||||
input := jsonschema.Performer{
|
||||
Name: "test",
|
||||
CareerLength: "2005 - 2015",
|
||||
}
|
||||
|
||||
p, err := performerJSONToPerformer(input)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, &startYear, p.CareerStart)
|
||||
assert.Equal(t, &endYear, p.CareerEnd)
|
||||
})
|
||||
|
||||
// legacy career_length with only start year
|
||||
t.Run("legacy career_length start only", func(t *testing.T) {
|
||||
input := jsonschema.Performer{
|
||||
Name: "test",
|
||||
CareerLength: "2005 -",
|
||||
}
|
||||
|
||||
p, err := performerJSONToPerformer(input)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, &startYear, p.CareerStart)
|
||||
assert.Nil(t, p.CareerEnd)
|
||||
})
|
||||
|
||||
// unparseable career_length should return an error
|
||||
t.Run("legacy career_length unparseable", func(t *testing.T) {
|
||||
input := jsonschema.Performer{
|
||||
Name: "test",
|
||||
CareerLength: "not a year range",
|
||||
}
|
||||
|
||||
_, err := performerJSONToPerformer(input)
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
|
||||
// no career fields at all
|
||||
t.Run("no career fields", func(t *testing.T) {
|
||||
input := jsonschema.Performer{
|
||||
Name: "test",
|
||||
}
|
||||
|
||||
p, err := performerJSONToPerformer(input)
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, p.CareerStart)
|
||||
assert.Nil(t, p.CareerEnd)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,6 +140,8 @@ func (r mappedResult) scrapedPerformer() *models.ScrapedPerformer {
|
|||
PenisLength: r.stringPtr("PenisLength"),
|
||||
Circumcised: r.stringPtr("Circumcised"),
|
||||
CareerLength: r.stringPtr("CareerLength"),
|
||||
CareerStart: r.IntPtr("CareerStart"),
|
||||
CareerEnd: r.IntPtr("CareerEnd"),
|
||||
Tattoos: r.stringPtr("Tattoos"),
|
||||
Piercings: r.stringPtr("Piercings"),
|
||||
Aliases: r.stringPtr("Aliases"),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ type ScrapedPerformerInput struct {
|
|||
PenisLength *string `json:"penis_length"`
|
||||
Circumcised *string `json:"circumcised"`
|
||||
CareerLength *string `json:"career_length"`
|
||||
CareerStart *int `json:"career_start"`
|
||||
CareerEnd *int `json:"career_end"`
|
||||
Tattoos *string `json:"tattoos"`
|
||||
Piercings *string `json:"piercings"`
|
||||
Aliases *string `json:"aliases"`
|
||||
|
|
|
|||
|
|
@ -125,6 +125,20 @@ func (c *postScraper) postScrapePerformer(ctx context.Context, p models.ScrapedP
|
|||
}
|
||||
}
|
||||
|
||||
isEmptyStr := func(s *string) bool { return s == nil || *s == "" }
|
||||
isEmptyInt := func(s *int) bool { return s == nil || *s == 0 }
|
||||
|
||||
// populate career start/end from career length and vice versa
|
||||
if !isEmptyStr(p.CareerLength) && isEmptyInt(p.CareerStart) && isEmptyInt(p.CareerEnd) {
|
||||
p.CareerStart, p.CareerEnd, err = utils.ParseYearRangeString(*p.CareerLength)
|
||||
if err != nil {
|
||||
logger.Warnf("Could not parse career length %s: %v", *p.CareerLength, err)
|
||||
}
|
||||
} else if isEmptyStr(p.CareerLength) && (!isEmptyInt(p.CareerStart) || !isEmptyInt(p.CareerEnd)) {
|
||||
v := utils.FormatYearRange(p.CareerStart, p.CareerEnd)
|
||||
p.CareerLength = &v
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const (
|
|||
cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE"
|
||||
)
|
||||
|
||||
var appSchemaVersion uint = 77
|
||||
var appSchemaVersion uint = 78
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsBox embed.FS
|
||||
|
|
|
|||
2
pkg/sqlite/migrations/78_performer_career_dates.up.sql
Normal file
2
pkg/sqlite/migrations/78_performer_career_dates.up.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "performers" ADD COLUMN "career_start" integer;
|
||||
ALTER TABLE "performers" ADD COLUMN "career_end" integer;
|
||||
143
pkg/sqlite/migrations/78_postmigrate.go
Normal file
143
pkg/sqlite/migrations/78_postmigrate.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/sqlite"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type schema78Migrator struct {
|
||||
migrator
|
||||
}
|
||||
|
||||
func post78(ctx context.Context, db *sqlx.DB) error {
|
||||
logger.Info("Running post-migration for schema version 78")
|
||||
|
||||
m := schema78Migrator{
|
||||
migrator: migrator{
|
||||
db: db,
|
||||
},
|
||||
}
|
||||
|
||||
if err := m.migrateCareerLength(ctx); err != nil {
|
||||
return fmt.Errorf("migrating career_length: %w", err)
|
||||
}
|
||||
|
||||
if err := m.dropCareerLength(); err != nil {
|
||||
return fmt.Errorf("dropping career_length column: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *schema78Migrator) migrateCareerLength(ctx context.Context) error {
|
||||
logger.Info("Migrating career_length to career_start/career_end")
|
||||
|
||||
const limit = 1000
|
||||
|
||||
lastID := 0
|
||||
parsed := 0
|
||||
unparseable := 0
|
||||
|
||||
for {
|
||||
gotSome := false
|
||||
|
||||
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
|
||||
query := `SELECT id, career_length FROM performers
|
||||
WHERE career_length IS NOT NULL AND career_length != ''`
|
||||
|
||||
if lastID != 0 {
|
||||
query += fmt.Sprintf(" AND id > %d", lastID)
|
||||
}
|
||||
|
||||
query += fmt.Sprintf(" ORDER BY id LIMIT %d", limit)
|
||||
|
||||
rows, err := tx.Query(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
id int
|
||||
careerLength string
|
||||
)
|
||||
|
||||
if err := rows.Scan(&id, &careerLength); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lastID = id
|
||||
gotSome = true
|
||||
|
||||
start, end, err := utils.ParseYearRangeString(careerLength)
|
||||
if err != nil {
|
||||
logger.Warnf("Could not parse career_length %q for performer %d: %v — preserving as custom field", careerLength, id, err)
|
||||
|
||||
if err := m.preserveAsCustomField(tx, id, careerLength); err != nil {
|
||||
return fmt.Errorf("preserving career_length for performer %d: %w", id, err)
|
||||
}
|
||||
unparseable++
|
||||
continue
|
||||
}
|
||||
|
||||
if err := m.updateCareerFields(tx, id, start, end); err != nil {
|
||||
return fmt.Errorf("updating career fields for performer %d: %w", id, err)
|
||||
}
|
||||
parsed++
|
||||
}
|
||||
|
||||
return rows.Err()
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !gotSome {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("Career length migration complete: %d parsed, %d unparseable (preserved as custom fields)", parsed, unparseable)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *schema78Migrator) updateCareerFields(tx *sqlx.Tx, id int, start *int, end *int) error {
|
||||
_, err := tx.Exec(
|
||||
"UPDATE performers SET career_start = ?, career_end = ? WHERE id = ?",
|
||||
start, end, id,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *schema78Migrator) preserveAsCustomField(tx *sqlx.Tx, id int, value string) error {
|
||||
// check if a career_length custom field already exists
|
||||
var existing sql.NullString
|
||||
err := tx.Get(&existing, "SELECT value FROM performer_custom_fields WHERE performer_id = ? AND field = 'career_length'", id)
|
||||
if err == nil {
|
||||
logger.Debugf("career_length custom field already exists for performer %d, skipping", id)
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = tx.Exec(
|
||||
"INSERT INTO performer_custom_fields (performer_id, field, value) VALUES (?, 'career_length', ?)",
|
||||
id, value,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *schema78Migrator) dropCareerLength() error {
|
||||
logger.Info("Dropping career_length column from performers table")
|
||||
return m.execAll([]string{
|
||||
"ALTER TABLE performers DROP COLUMN career_length",
|
||||
})
|
||||
}
|
||||
|
||||
func init() {
|
||||
sqlite.RegisterPostMigration(78, post78)
|
||||
}
|
||||
|
|
@ -44,7 +44,8 @@ type performerRow struct {
|
|||
FakeTits zero.String `db:"fake_tits"`
|
||||
PenisLength null.Float `db:"penis_length"`
|
||||
Circumcised zero.String `db:"circumcised"`
|
||||
CareerLength zero.String `db:"career_length"`
|
||||
CareerStart null.Int `db:"career_start"`
|
||||
CareerEnd null.Int `db:"career_end"`
|
||||
Tattoos zero.String `db:"tattoos"`
|
||||
Piercings zero.String `db:"piercings"`
|
||||
Favorite bool `db:"favorite"`
|
||||
|
|
@ -82,7 +83,8 @@ func (r *performerRow) fromPerformer(o models.Performer) {
|
|||
if o.Circumcised != nil && o.Circumcised.IsValid() {
|
||||
r.Circumcised = zero.StringFrom(o.Circumcised.String())
|
||||
}
|
||||
r.CareerLength = zero.StringFrom(o.CareerLength)
|
||||
r.CareerStart = intFromPtr(o.CareerStart)
|
||||
r.CareerEnd = intFromPtr(o.CareerEnd)
|
||||
r.Tattoos = zero.StringFrom(o.Tattoos)
|
||||
r.Piercings = zero.StringFrom(o.Piercings)
|
||||
r.Favorite = o.Favorite
|
||||
|
|
@ -110,7 +112,8 @@ func (r *performerRow) resolve() *models.Performer {
|
|||
Measurements: r.Measurements.String,
|
||||
FakeTits: r.FakeTits.String,
|
||||
PenisLength: nullFloatPtr(r.PenisLength),
|
||||
CareerLength: r.CareerLength.String,
|
||||
CareerStart: nullIntPtr(r.CareerStart),
|
||||
CareerEnd: nullIntPtr(r.CareerEnd),
|
||||
Tattoos: r.Tattoos.String,
|
||||
Piercings: r.Piercings.String,
|
||||
Favorite: r.Favorite,
|
||||
|
|
@ -155,7 +158,8 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) {
|
|||
r.setNullString("fake_tits", o.FakeTits)
|
||||
r.setNullFloat64("penis_length", o.PenisLength)
|
||||
r.setNullString("circumcised", o.Circumcised)
|
||||
r.setNullString("career_length", o.CareerLength)
|
||||
r.setNullInt("career_start", o.CareerStart)
|
||||
r.setNullInt("career_end", o.CareerEnd)
|
||||
r.setNullString("tattoos", o.Tattoos)
|
||||
r.setNullString("piercings", o.Piercings)
|
||||
r.setBool("favorite", o.Favorite)
|
||||
|
|
@ -776,7 +780,8 @@ func (qb *PerformerStore) sortByScenesDuration(direction string) string {
|
|||
|
||||
var performerSortOptions = sortOptions{
|
||||
"birthdate",
|
||||
"career_length",
|
||||
"career_start",
|
||||
"career_end",
|
||||
"created_at",
|
||||
"galleries_count",
|
||||
"height",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,29 @@ func (qb *performerFilterHandler) validate() error {
|
|||
}
|
||||
}
|
||||
|
||||
// if legacy career length filter used, ensure only supported modifiers are used and value is valid
|
||||
if filter.CareerLength != nil {
|
||||
careerLength := filter.CareerLength
|
||||
switch careerLength.Modifier {
|
||||
case models.CriterionModifierEquals:
|
||||
start, end, err := utils.ParseYearRangeString(careerLength.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid career length value: %s", careerLength.Value)
|
||||
}
|
||||
// ensure career start/end is not set
|
||||
if start != nil && filter.CareerStart != nil {
|
||||
return fmt.Errorf("cannot use legacy CareerLength filter with CareerStart filter")
|
||||
}
|
||||
if end != nil && filter.CareerEnd != nil {
|
||||
return fmt.Errorf("cannot use legacy CareerLength filter with CareerEnd filter")
|
||||
}
|
||||
case models.CriterionModifierIsNull, models.CriterionModifierNotNull:
|
||||
// valid modifiers, no value parsing needed
|
||||
default:
|
||||
return fmt.Errorf("invalid career length modifier: %s", careerLength.Modifier)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -71,10 +94,13 @@ func (qb *performerFilterHandler) handle(ctx context.Context, f *filterBuilder)
|
|||
}
|
||||
|
||||
func (qb *performerFilterHandler) criterionHandler() criterionHandler {
|
||||
filter := qb.performerFilter
|
||||
// make a copy of the filter to modify with legacy conversions without affecting original filter used for subfilters
|
||||
filter := *qb.performerFilter
|
||||
const tableName = performerTable
|
||||
heightCmCrit := filter.HeightCm
|
||||
|
||||
convertLegacyCareerLengthFilter(&filter)
|
||||
|
||||
return compoundHandler{
|
||||
stringCriterionHandler(filter.Name, tableName+".name"),
|
||||
stringCriterionHandler(filter.Disambiguation, tableName+".disambiguation"),
|
||||
|
|
@ -129,7 +155,9 @@ func (qb *performerFilterHandler) criterionHandler() criterionHandler {
|
|||
}
|
||||
}),
|
||||
|
||||
stringCriterionHandler(filter.CareerLength, tableName+".career_length"),
|
||||
// CareerLength filter is deprecated and non-functional (column removed in schema 78)
|
||||
intCriterionHandler(filter.CareerStart, tableName+".career_start", nil),
|
||||
intCriterionHandler(filter.CareerEnd, tableName+".career_end", nil),
|
||||
stringCriterionHandler(filter.Tattoos, tableName+".tattoos"),
|
||||
stringCriterionHandler(filter.Piercings, tableName+".piercings"),
|
||||
intCriterionHandler(filter.Rating100, tableName+".rating", nil),
|
||||
|
|
@ -221,6 +249,43 @@ func (qb *performerFilterHandler) criterionHandler() criterionHandler {
|
|||
}
|
||||
}
|
||||
|
||||
func convertLegacyCareerLengthFilter(filter *models.PerformerFilterType) {
|
||||
// convert legacy career length filter to career start/end filters
|
||||
if filter.CareerLength != nil {
|
||||
careerLength := filter.CareerLength
|
||||
switch careerLength.Modifier {
|
||||
case models.CriterionModifierEquals:
|
||||
start, end, _ := utils.ParseYearRangeString(careerLength.Value)
|
||||
if start != nil {
|
||||
filter.CareerStart = &models.IntCriterionInput{
|
||||
Value: (*start) - 1, // minus one to make it exclusive
|
||||
Modifier: models.CriterionModifierGreaterThan,
|
||||
}
|
||||
}
|
||||
if end != nil {
|
||||
filter.CareerEnd = &models.IntCriterionInput{
|
||||
Value: (*end) + 1, // plus one to make it exclusive
|
||||
Modifier: models.CriterionModifierLessThan,
|
||||
}
|
||||
}
|
||||
case models.CriterionModifierIsNull:
|
||||
filter.CareerStart = &models.IntCriterionInput{
|
||||
Modifier: models.CriterionModifierIsNull,
|
||||
}
|
||||
filter.CareerEnd = &models.IntCriterionInput{
|
||||
Modifier: models.CriterionModifierIsNull,
|
||||
}
|
||||
case models.CriterionModifierNotNull:
|
||||
filter.CareerStart = &models.IntCriterionInput{
|
||||
Modifier: models.CriterionModifierNotNull,
|
||||
}
|
||||
filter.CareerEnd = &models.IntCriterionInput{
|
||||
Modifier: models.CriterionModifierNotNull,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - we need to provide a whitelist of possible values
|
||||
func (qb *performerFilterHandler) performerIsMissingCriterionHandler(isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ func Test_PerformerStore_Create(t *testing.T) {
|
|||
fakeTits = "fakeTits"
|
||||
penisLength = 1.23
|
||||
circumcised = models.CircumisedEnumCut
|
||||
careerLength = "careerLength"
|
||||
careerStart = 2005
|
||||
careerEnd = 2015
|
||||
tattoos = "tattoos"
|
||||
piercings = "piercings"
|
||||
aliases = []string{"alias1", "alias2"}
|
||||
|
|
@ -107,7 +108,8 @@ func Test_PerformerStore_Create(t *testing.T) {
|
|||
FakeTits: fakeTits,
|
||||
PenisLength: &penisLength,
|
||||
Circumcised: &circumcised,
|
||||
CareerLength: careerLength,
|
||||
CareerStart: &careerStart,
|
||||
CareerEnd: &careerEnd,
|
||||
Tattoos: tattoos,
|
||||
Piercings: piercings,
|
||||
Favorite: favorite,
|
||||
|
|
@ -204,8 +206,6 @@ func Test_PerformerStore_Create(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(tt.newObject.CustomFields, cf)
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -229,7 +229,8 @@ func Test_PerformerStore_Update(t *testing.T) {
|
|||
fakeTits = "fakeTits"
|
||||
penisLength = 1.23
|
||||
circumcised = models.CircumisedEnumCut
|
||||
careerLength = "careerLength"
|
||||
careerStart = 2005
|
||||
careerEnd = 2015
|
||||
tattoos = "tattoos"
|
||||
piercings = "piercings"
|
||||
aliases = []string{"alias1", "alias2"}
|
||||
|
|
@ -271,7 +272,8 @@ func Test_PerformerStore_Update(t *testing.T) {
|
|||
FakeTits: fakeTits,
|
||||
PenisLength: &penisLength,
|
||||
Circumcised: &circumcised,
|
||||
CareerLength: careerLength,
|
||||
CareerStart: &careerStart,
|
||||
CareerEnd: &careerEnd,
|
||||
Tattoos: tattoos,
|
||||
Piercings: piercings,
|
||||
Favorite: favorite,
|
||||
|
|
@ -422,7 +424,8 @@ func clearPerformerPartial() models.PerformerPartial {
|
|||
FakeTits: nullString,
|
||||
PenisLength: nullFloat,
|
||||
Circumcised: nullString,
|
||||
CareerLength: nullString,
|
||||
CareerStart: nullInt,
|
||||
CareerEnd: nullInt,
|
||||
Tattoos: nullString,
|
||||
Piercings: nullString,
|
||||
Aliases: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet},
|
||||
|
|
@ -455,7 +458,8 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
|||
fakeTits = "fakeTits"
|
||||
penisLength = 1.23
|
||||
circumcised = models.CircumisedEnumCut
|
||||
careerLength = "careerLength"
|
||||
careerStart = 2005
|
||||
careerEnd = 2015
|
||||
tattoos = "tattoos"
|
||||
piercings = "piercings"
|
||||
aliases = []string{"alias1", "alias2"}
|
||||
|
|
@ -501,7 +505,8 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
|||
FakeTits: models.NewOptionalString(fakeTits),
|
||||
PenisLength: models.NewOptionalFloat64(penisLength),
|
||||
Circumcised: models.NewOptionalString(circumcised.String()),
|
||||
CareerLength: models.NewOptionalString(careerLength),
|
||||
CareerStart: models.NewOptionalInt(careerStart),
|
||||
CareerEnd: models.NewOptionalInt(careerEnd),
|
||||
Tattoos: models.NewOptionalString(tattoos),
|
||||
Piercings: models.NewOptionalString(piercings),
|
||||
Aliases: &models.UpdateStrings{
|
||||
|
|
@ -552,7 +557,8 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
|||
FakeTits: fakeTits,
|
||||
PenisLength: &penisLength,
|
||||
Circumcised: &circumcised,
|
||||
CareerLength: careerLength,
|
||||
CareerStart: &careerStart,
|
||||
CareerEnd: &careerEnd,
|
||||
Tattoos: tattoos,
|
||||
Piercings: piercings,
|
||||
Aliases: models.NewRelatedStrings(aliases),
|
||||
|
|
@ -1766,30 +1772,117 @@ func verifyPerformerAge(t *testing.T, ageCriterion models.IntCriterionInput) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPerformerQueryCareerLength(t *testing.T) {
|
||||
const value = "2005"
|
||||
careerLengthCriterion := models.StringCriterionInput{
|
||||
func TestPerformerQueryLegacyCareerLength(t *testing.T) {
|
||||
const value = "2002 - 2012"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
c models.StringCriterionInput
|
||||
careerStartCrit *models.IntCriterionInput
|
||||
careerEndCrit *models.IntCriterionInput
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
name: "valid format",
|
||||
c: models.StringCriterionInput{
|
||||
Value: value,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
careerStartCrit: &models.IntCriterionInput{
|
||||
Value: 2002,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
careerEndCrit: &models.IntCriterionInput{
|
||||
Value: 2012,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
name: "invalid format",
|
||||
c: models.StringCriterionInput{
|
||||
Value: "invalid format",
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "is null",
|
||||
c: models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierIsNull,
|
||||
},
|
||||
careerStartCrit: &models.IntCriterionInput{
|
||||
Modifier: models.CriterionModifierIsNull,
|
||||
},
|
||||
careerEndCrit: &models.IntCriterionInput{
|
||||
Modifier: models.CriterionModifierIsNull,
|
||||
},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
name: "not null",
|
||||
c: models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierNotNull,
|
||||
},
|
||||
careerStartCrit: &models.IntCriterionInput{
|
||||
Modifier: models.CriterionModifierNotNull,
|
||||
},
|
||||
careerEndCrit: &models.IntCriterionInput{
|
||||
Modifier: models.CriterionModifierNotNull,
|
||||
},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
name: "invalid modifier",
|
||||
c: models.StringCriterionInput{
|
||||
Value: value,
|
||||
Modifier: models.CriterionModifierMatchesRegex,
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
qb := db.Performer
|
||||
|
||||
for _, tt := range tests {
|
||||
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||
performers, _, err := qb.Query(ctx, &models.PerformerFilterType{
|
||||
CareerLength: &tt.c,
|
||||
}, nil)
|
||||
|
||||
if err != nil && !tt.err {
|
||||
t.Errorf("Error querying performer: %s", err.Error())
|
||||
} else if err == nil && tt.err {
|
||||
t.Errorf("Expected error but got none")
|
||||
}
|
||||
|
||||
if err != nil || tt.err {
|
||||
return
|
||||
}
|
||||
|
||||
if len(performers) == 0 {
|
||||
t.Errorf("Expected to find performers but found none")
|
||||
}
|
||||
|
||||
for _, performer := range performers {
|
||||
verifyIntPtr(t, performer.CareerStart, *tt.careerStartCrit)
|
||||
verifyIntPtr(t, performer.CareerEnd, *tt.careerEndCrit)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerformerQueryCareerStart(t *testing.T) {
|
||||
const value = 2002
|
||||
criterion := models.IntCriterionInput{
|
||||
Value: value,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
verifyPerformerCareerLength(t, careerLengthCriterion)
|
||||
|
||||
careerLengthCriterion.Modifier = models.CriterionModifierNotEquals
|
||||
verifyPerformerCareerLength(t, careerLengthCriterion)
|
||||
|
||||
careerLengthCriterion.Modifier = models.CriterionModifierMatchesRegex
|
||||
verifyPerformerCareerLength(t, careerLengthCriterion)
|
||||
|
||||
careerLengthCriterion.Modifier = models.CriterionModifierNotMatchesRegex
|
||||
verifyPerformerCareerLength(t, careerLengthCriterion)
|
||||
}
|
||||
|
||||
func verifyPerformerCareerLength(t *testing.T, criterion models.StringCriterionInput) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
qb := db.Performer
|
||||
performerFilter := models.PerformerFilterType{
|
||||
CareerLength: &criterion,
|
||||
CareerStart: &criterion,
|
||||
}
|
||||
|
||||
performers, _, err := qb.Query(ctx, &performerFilter, nil)
|
||||
|
|
@ -1798,8 +1891,33 @@ func verifyPerformerCareerLength(t *testing.T, criterion models.StringCriterionI
|
|||
}
|
||||
|
||||
for _, performer := range performers {
|
||||
cl := performer.CareerLength
|
||||
verifyString(t, cl, criterion)
|
||||
verifyIntPtr(t, performer.CareerStart, criterion)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestPerformerQueryCareerEnd(t *testing.T) {
|
||||
const value = 2012
|
||||
criterion := models.IntCriterionInput{
|
||||
Value: value,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
withTxn(func(ctx context.Context) error {
|
||||
qb := db.Performer
|
||||
performerFilter := models.PerformerFilterType{
|
||||
CareerEnd: &criterion,
|
||||
}
|
||||
|
||||
performers, _, err := qb.Query(ctx, &performerFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying performer: %s", err.Error())
|
||||
}
|
||||
|
||||
for _, performer := range performers {
|
||||
verifyIntPtr(t, performer.CareerEnd, criterion)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1513,15 +1513,28 @@ func getPerformerDeathDate(index int) *models.Date {
|
|||
return &ret
|
||||
}
|
||||
|
||||
func getPerformerCareerLength(index int) *string {
|
||||
func getPerformerCareerStart(index int) *int {
|
||||
if index%5 == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := fmt.Sprintf("20%2d", index)
|
||||
ret := 2000 + index
|
||||
return &ret
|
||||
}
|
||||
|
||||
func getPerformerCareerEnd(index int) *int {
|
||||
if index%5 == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// only set career_end for even indices
|
||||
if index%2 == 0 {
|
||||
ret := 2010 + index
|
||||
return &ret
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPerformerPenisLength(index int) *float64 {
|
||||
if index%5 == 0 {
|
||||
return nil
|
||||
|
|
@ -1615,10 +1628,8 @@ func createPerformers(ctx context.Context, n int, o int) error {
|
|||
TagIDs: models.NewRelatedIDs(tids),
|
||||
}
|
||||
|
||||
careerLength := getPerformerCareerLength(i)
|
||||
if careerLength != nil {
|
||||
performer.CareerLength = *careerLength
|
||||
}
|
||||
performer.CareerStart = getPerformerCareerStart(i)
|
||||
performer.CareerEnd = getPerformerCareerEnd(i)
|
||||
|
||||
if (index+1)%5 != 0 {
|
||||
performer.StashIDs = models.NewRelatedStashIDs([]models.StashID{
|
||||
|
|
|
|||
|
|
@ -231,6 +231,16 @@ func performerFragmentToScrapedPerformer(p graphql.PerformerFragment) *models.Sc
|
|||
sp.Height = &hs
|
||||
}
|
||||
|
||||
if p.CareerStartYear != nil {
|
||||
cs := *p.CareerStartYear
|
||||
sp.CareerStart = &cs
|
||||
}
|
||||
|
||||
if p.CareerEndYear != nil {
|
||||
ce := *p.CareerEndYear
|
||||
sp.CareerEnd = &ce
|
||||
}
|
||||
|
||||
if p.BirthDate != nil {
|
||||
sp.Birthdate = padFuzzyDate(p.BirthDate)
|
||||
}
|
||||
|
|
@ -388,16 +398,11 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf
|
|||
aliases := strings.Join(performer.Aliases.List(), ",")
|
||||
draft.Aliases = &aliases
|
||||
}
|
||||
if performer.CareerLength != "" {
|
||||
var career = strings.Split(performer.CareerLength, "-")
|
||||
if i, err := strconv.Atoi(strings.TrimSpace(career[0])); err == nil {
|
||||
draft.CareerStartYear = &i
|
||||
}
|
||||
if len(career) == 2 {
|
||||
if y, err := strconv.Atoi(strings.TrimSpace(career[1])); err == nil {
|
||||
draft.CareerEndYear = &y
|
||||
}
|
||||
}
|
||||
if performer.CareerStart != nil {
|
||||
draft.CareerStartYear = performer.CareerStart
|
||||
}
|
||||
if performer.CareerEnd != nil {
|
||||
draft.CareerEndYear = performer.CareerEnd
|
||||
}
|
||||
|
||||
if len(performer.URLs.List()) > 0 {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package utils
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -25,3 +27,80 @@ func ParseDateStringAsTime(dateString string) (time.Time, error) {
|
|||
|
||||
return time.Time{}, fmt.Errorf("ParseDateStringAsTime failed: dateString <%s>", dateString)
|
||||
}
|
||||
|
||||
// ParseYearRangeString parses a year range string into start and end year integers.
|
||||
// Supported formats: "YYYY", "YYYY - YYYY", "YYYY-YYYY", "YYYY -", "- YYYY", "YYYY-present".
|
||||
// Returns nil for start/end if not present in the string.
|
||||
func ParseYearRangeString(s string) (start *int, end *int, err error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil, nil, fmt.Errorf("empty year range string")
|
||||
}
|
||||
|
||||
// normalize "present" to empty end
|
||||
lower := strings.ToLower(s)
|
||||
lower = strings.ReplaceAll(lower, "present", "")
|
||||
|
||||
// split on "-" if it contains one
|
||||
var parts []string
|
||||
if strings.Contains(lower, "-") {
|
||||
parts = strings.SplitN(lower, "-", 2)
|
||||
} else {
|
||||
// single value, treat as start year
|
||||
year, err := parseYear(lower)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid year range %q: %w", s, err)
|
||||
}
|
||||
return &year, nil, nil
|
||||
}
|
||||
|
||||
startStr := strings.TrimSpace(parts[0])
|
||||
endStr := strings.TrimSpace(parts[1])
|
||||
|
||||
if startStr != "" {
|
||||
y, err := parseYear(startStr)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid start year in %q: %w", s, err)
|
||||
}
|
||||
start = &y
|
||||
}
|
||||
|
||||
if endStr != "" {
|
||||
y, err := parseYear(endStr)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid end year in %q: %w", s, err)
|
||||
}
|
||||
end = &y
|
||||
}
|
||||
|
||||
if start == nil && end == nil {
|
||||
return nil, nil, fmt.Errorf("could not parse year range %q", s)
|
||||
}
|
||||
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
func parseYear(s string) (int, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
year, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid year %q: %w", s, err)
|
||||
}
|
||||
if year < 1900 || year > 2200 {
|
||||
return 0, fmt.Errorf("year %d out of reasonable range", year)
|
||||
}
|
||||
return year, nil
|
||||
}
|
||||
|
||||
func FormatYearRange(start *int, end *int) string {
|
||||
switch {
|
||||
case start == nil && end == nil:
|
||||
return ""
|
||||
case end == nil:
|
||||
return fmt.Sprintf("%d -", *start)
|
||||
case start == nil:
|
||||
return fmt.Sprintf("- %d", *end)
|
||||
default:
|
||||
return fmt.Sprintf("%d - %d", *start, *end)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package utils
|
|||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseDateStringAsTime(t *testing.T) {
|
||||
|
|
@ -41,3 +43,66 @@ func TestParseDateStringAsTime(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseYearRangeString(t *testing.T) {
|
||||
intPtr := func(v int) *int { return &v }
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantStart *int
|
||||
wantEnd *int
|
||||
wantErr bool
|
||||
}{
|
||||
{"single year", "2005", intPtr(2005), nil, false},
|
||||
{"year range with spaces", "2005 - 2010", intPtr(2005), intPtr(2010), false},
|
||||
{"year range no spaces", "2005-2010", intPtr(2005), intPtr(2010), false},
|
||||
{"year dash open", "2005 -", intPtr(2005), nil, false},
|
||||
{"year dash open no space", "2005-", intPtr(2005), nil, false},
|
||||
{"dash year", "- 2010", nil, intPtr(2010), false},
|
||||
{"year present", "2005-present", intPtr(2005), nil, false},
|
||||
{"year Present caps", "2005 - Present", intPtr(2005), nil, false},
|
||||
{"whitespace padding", " 2005 - 2010 ", intPtr(2005), intPtr(2010), false},
|
||||
{"empty string", "", nil, nil, true},
|
||||
{"garbage", "not a year", nil, nil, true},
|
||||
{"partial garbage start", "abc - 2010", nil, nil, true},
|
||||
{"partial garbage end", "2005 - abc", nil, nil, true},
|
||||
{"year out of range", "1800", nil, nil, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
start, end, err := ParseYearRangeString(tt.input)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantStart, start)
|
||||
assert.Equal(t, tt.wantEnd, end)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatYearRange(t *testing.T) {
|
||||
intPtr := func(v int) *int { return &v }
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
start *int
|
||||
end *int
|
||||
want string
|
||||
}{
|
||||
{"both nil", nil, nil, ""},
|
||||
{"only start", intPtr(2005), nil, "2005 -"},
|
||||
{"only end", nil, intPtr(2010), "- 2010"},
|
||||
{"start and end", intPtr(2005), intPtr(2010), "2005 - 2010"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := FormatYearRange(tt.start, tt.end)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ fragment SlimPerformerData on Performer {
|
|||
fake_tits
|
||||
penis_length
|
||||
circumcised
|
||||
career_length
|
||||
career_start
|
||||
career_end
|
||||
tattoos
|
||||
piercings
|
||||
alias_list
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ fragment PerformerData on Performer {
|
|||
fake_tits
|
||||
penis_length
|
||||
circumcised
|
||||
career_length
|
||||
career_start
|
||||
career_end
|
||||
tattoos
|
||||
piercings
|
||||
alias_list
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ fragment ScrapedPerformerData on ScrapedPerformer {
|
|||
fake_tits
|
||||
penis_length
|
||||
circumcised
|
||||
career_length
|
||||
career_start
|
||||
career_end
|
||||
tattoos
|
||||
piercings
|
||||
aliases
|
||||
|
|
@ -68,7 +69,8 @@ fragment ScrapedScenePerformerData on ScrapedPerformer {
|
|||
fake_tits
|
||||
penis_length
|
||||
circumcised
|
||||
career_length
|
||||
career_start
|
||||
career_end
|
||||
tattoos
|
||||
piercings
|
||||
aliases
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ const performerFields = [
|
|||
"gender",
|
||||
"birthdate",
|
||||
"death_date",
|
||||
"career_length",
|
||||
"career_start",
|
||||
"career_end",
|
||||
"country",
|
||||
"ethnicity",
|
||||
"eye_color",
|
||||
|
|
@ -363,8 +364,15 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
{renderTextField("piercings", updateInput.piercings, (v) =>
|
||||
setUpdateField({ piercings: v })
|
||||
)}
|
||||
{renderTextField("career_length", updateInput.career_length, (v) =>
|
||||
setUpdateField({ career_length: v })
|
||||
{renderTextField(
|
||||
"career_start",
|
||||
updateInput.career_start?.toString(),
|
||||
(v) => setUpdateField({ career_start: v ? parseInt(v) : undefined })
|
||||
)}
|
||||
{renderTextField(
|
||||
"career_end",
|
||||
updateInput.career_end?.toString(),
|
||||
(v) => setUpdateField({ career_end: v ? parseInt(v) : undefined })
|
||||
)}
|
||||
|
||||
<Form.Group controlId="tags">
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
FormatHeight,
|
||||
FormatPenisLength,
|
||||
FormatWeight,
|
||||
formatYearRange,
|
||||
} from "../PerformerList";
|
||||
import { PatchComponent } from "src/patch";
|
||||
import { CustomFields } from "src/components/Shared/CustomFields";
|
||||
|
|
@ -174,7 +175,10 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> =
|
|||
/>
|
||||
<DetailItem
|
||||
id="career_length"
|
||||
value={performer?.career_length}
|
||||
value={formatYearRange(
|
||||
performer?.career_start,
|
||||
performer?.career_end
|
||||
)}
|
||||
fullWidth={fullWidth}
|
||||
/>
|
||||
<DetailItem id="details" value={details} fullWidth={fullWidth} />
|
||||
|
|
|
|||
|
|
@ -126,7 +126,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
circumcised: yupInputEnum(GQL.CircumisedEnum).nullable().defined(),
|
||||
tattoos: yup.string().ensure(),
|
||||
piercings: yup.string().ensure(),
|
||||
career_length: yup.string().ensure(),
|
||||
career_start: yupInputNumber().positive().nullable().defined(),
|
||||
career_end: yupInputNumber().positive().nullable().defined(),
|
||||
urls: yupUniqueStringList(intl),
|
||||
details: yup.string().ensure(),
|
||||
tag_ids: yup.array(yup.string().required()).defined(),
|
||||
|
|
@ -155,7 +156,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
circumcised: performer.circumcised ?? null,
|
||||
tattoos: performer.tattoos ?? "",
|
||||
piercings: performer.piercings ?? "",
|
||||
career_length: performer.career_length ?? "",
|
||||
career_start: performer.career_start ?? null,
|
||||
career_end: performer.career_end ?? null,
|
||||
urls: performer.urls ?? [],
|
||||
details: performer.details ?? "",
|
||||
tag_ids: (performer.tags ?? []).map((t) => t.id),
|
||||
|
|
@ -256,8 +258,11 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
if (state.fake_tits) {
|
||||
formik.setFieldValue("fake_tits", state.fake_tits);
|
||||
}
|
||||
if (state.career_length) {
|
||||
formik.setFieldValue("career_length", state.career_length);
|
||||
if (state.career_start) {
|
||||
formik.setFieldValue("career_start", state.career_start);
|
||||
}
|
||||
if (state.career_end) {
|
||||
formik.setFieldValue("career_end", state.career_end);
|
||||
}
|
||||
if (state.tattoos) {
|
||||
formik.setFieldValue("tattoos", state.tattoos);
|
||||
|
|
@ -747,7 +752,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
{renderInputField("tattoos", "textarea")}
|
||||
{renderInputField("piercings", "textarea")}
|
||||
|
||||
{renderInputField("career_length")}
|
||||
{renderInputField("career_start", "number")}
|
||||
{renderInputField("career_end", "number")}
|
||||
|
||||
{renderURLListField("urls", onScrapePerformerURL, urlScrapable)}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
ScrapedTextAreaRow,
|
||||
ScrapedCountryRow,
|
||||
ScrapedStringListRow,
|
||||
ScrapedNumberRow,
|
||||
} from "src/components/Shared/ScrapeDialog/ScrapeDialogRow";
|
||||
import { ScrapeDialog } from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||
import { Form } from "react-bootstrap";
|
||||
|
|
@ -272,10 +273,16 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
const [fakeTits, setFakeTits] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.fake_tits, props.scraped.fake_tits)
|
||||
);
|
||||
const [careerLength, setCareerLength] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(
|
||||
props.performer.career_length,
|
||||
props.scraped.career_length
|
||||
const [careerStart, setCareerStart] = useState<ScrapeResult<number>>(
|
||||
new ScrapeResult<number>(
|
||||
props.performer.career_start,
|
||||
props.scraped.career_start
|
||||
)
|
||||
);
|
||||
const [careerEnd, setCareerEnd] = useState<ScrapeResult<number>>(
|
||||
new ScrapeResult<number>(
|
||||
props.performer.career_end,
|
||||
props.scraped.career_end
|
||||
)
|
||||
);
|
||||
const [tattoos, setTattoos] = useState<ScrapeResult<string>>(
|
||||
|
|
@ -347,7 +354,8 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
fakeTits,
|
||||
penisLength,
|
||||
circumcised,
|
||||
careerLength,
|
||||
careerStart,
|
||||
careerEnd,
|
||||
tattoos,
|
||||
piercings,
|
||||
urls,
|
||||
|
|
@ -379,7 +387,8 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
height: height.getNewValue(),
|
||||
measurements: measurements.getNewValue(),
|
||||
fake_tits: fakeTits.getNewValue(),
|
||||
career_length: careerLength.getNewValue(),
|
||||
career_start: careerStart.getNewValue(),
|
||||
career_end: careerEnd.getNewValue(),
|
||||
tattoos: tattoos.getNewValue(),
|
||||
piercings: piercings.getNewValue(),
|
||||
urls: urls.getNewValue(),
|
||||
|
|
@ -493,11 +502,17 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
result={fakeTits}
|
||||
onChange={(value) => setFakeTits(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="career_length"
|
||||
title={intl.formatMessage({ id: "career_length" })}
|
||||
result={careerLength}
|
||||
onChange={(value) => setCareerLength(value)}
|
||||
<ScrapedNumberRow
|
||||
field="career_start"
|
||||
title={intl.formatMessage({ id: "career_start" })}
|
||||
result={careerStart}
|
||||
onChange={(value) => setCareerStart(value)}
|
||||
/>
|
||||
<ScrapedNumberRow
|
||||
field="career_end"
|
||||
title={intl.formatMessage({ id: "career_end" })}
|
||||
result={careerEnd}
|
||||
onChange={(value) => setCareerEnd(value)}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
field="tattoos"
|
||||
|
|
|
|||
|
|
@ -137,6 +137,14 @@ export const FormatWeight = (weight?: number | null) => {
|
|||
);
|
||||
};
|
||||
|
||||
export function formatYearRange(
|
||||
start?: number | null,
|
||||
end?: number | null
|
||||
): string | undefined {
|
||||
if (!start && !end) return undefined;
|
||||
return `${start ?? ""} - ${end ?? ""}`;
|
||||
}
|
||||
|
||||
export const FormatCircumcised = (circumcised?: GQL.CircumisedEnum | null) => {
|
||||
const intl = useIntl();
|
||||
if (!circumcised) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
FormatHeight,
|
||||
FormatPenisLength,
|
||||
FormatWeight,
|
||||
formatYearRange,
|
||||
} from "./PerformerList";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { getCountryByISO } from "src/utils/country";
|
||||
|
|
@ -188,7 +189,7 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
|
|||
);
|
||||
|
||||
const CareerLengthCell = (performer: GQL.PerformerDataFragment) => (
|
||||
<span className="ellips-data">{performer.career_length}</span>
|
||||
<>{formatYearRange(performer.career_start, performer.career_end) ?? ""}</>
|
||||
);
|
||||
|
||||
const SceneCountCell = (performer: GQL.PerformerDataFragment) => (
|
||||
|
|
|
|||
|
|
@ -102,8 +102,11 @@ const PerformerMergeDetails: React.FC<IPerformerMergeDetailsProps> = ({
|
|||
const [fakeTits, setFakeTits] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(dest.fake_tits)
|
||||
);
|
||||
const [careerLength, setCareerLength] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(dest.career_length)
|
||||
const [careerStart, setCareerStart] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(dest.career_start?.toString())
|
||||
);
|
||||
const [careerEnd, setCareerEnd] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(dest.career_end?.toString())
|
||||
);
|
||||
const [tattoos, setTattoos] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(dest.tattoos)
|
||||
|
|
@ -264,11 +267,18 @@ const PerformerMergeDetails: React.FC<IPerformerMergeDetailsProps> = ({
|
|||
!dest.fake_tits
|
||||
)
|
||||
);
|
||||
setCareerLength(
|
||||
setCareerStart(
|
||||
new ScrapeResult(
|
||||
dest.career_length,
|
||||
sources.find((s) => s.career_length)?.career_length,
|
||||
!dest.career_length
|
||||
dest.career_start?.toString(),
|
||||
sources.find((s) => s.career_start)?.career_start?.toString(),
|
||||
!dest.career_start
|
||||
)
|
||||
);
|
||||
setCareerEnd(
|
||||
new ScrapeResult(
|
||||
dest.career_end?.toString(),
|
||||
sources.find((s) => s.career_end)?.career_end?.toString(),
|
||||
!dest.career_end
|
||||
)
|
||||
);
|
||||
setTattoos(
|
||||
|
|
@ -378,7 +388,8 @@ const PerformerMergeDetails: React.FC<IPerformerMergeDetailsProps> = ({
|
|||
penisLength,
|
||||
measurements,
|
||||
fakeTits,
|
||||
careerLength,
|
||||
careerStart,
|
||||
careerEnd,
|
||||
tattoos,
|
||||
piercings,
|
||||
urls,
|
||||
|
|
@ -404,7 +415,8 @@ const PerformerMergeDetails: React.FC<IPerformerMergeDetailsProps> = ({
|
|||
penisLength,
|
||||
measurements,
|
||||
fakeTits,
|
||||
careerLength,
|
||||
careerStart,
|
||||
careerEnd,
|
||||
tattoos,
|
||||
piercings,
|
||||
urls,
|
||||
|
|
@ -520,10 +532,16 @@ const PerformerMergeDetails: React.FC<IPerformerMergeDetailsProps> = ({
|
|||
onChange={(value) => setFakeTits(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="career_length"
|
||||
title={intl.formatMessage({ id: "career_length" })}
|
||||
result={careerLength}
|
||||
onChange={(value) => setCareerLength(value)}
|
||||
field="career_start"
|
||||
title={intl.formatMessage({ id: "career_start" })}
|
||||
result={careerStart}
|
||||
onChange={(value) => setCareerStart(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="career_end"
|
||||
title={intl.formatMessage({ id: "career_end" })}
|
||||
result={careerEnd}
|
||||
onChange={(value) => setCareerEnd(value)}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
field="tattoos"
|
||||
|
|
@ -612,7 +630,12 @@ const PerformerMergeDetails: React.FC<IPerformerMergeDetailsProps> = ({
|
|||
: undefined,
|
||||
measurements: measurements.getNewValue(),
|
||||
fake_tits: fakeTits.getNewValue(),
|
||||
career_length: careerLength.getNewValue(),
|
||||
career_start: careerStart.getNewValue()
|
||||
? parseInt(careerStart.getNewValue()!)
|
||||
: undefined,
|
||||
career_end: careerEnd.getNewValue()
|
||||
? parseInt(careerEnd.getNewValue()!)
|
||||
: undefined,
|
||||
tattoos: tattoos.getNewValue(),
|
||||
piercings: piercings.getNewValue(),
|
||||
urls: urls.getNewValue(),
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@
|
|||
.collapsed {
|
||||
.detail-item.tattoos,
|
||||
.detail-item.piercings,
|
||||
.detail-item.career_length,
|
||||
.detail-item.career_start,
|
||||
.detail-item.career_end,
|
||||
.detail-item.details,
|
||||
.detail-item.tags,
|
||||
.detail-item.stash_ids {
|
||||
|
|
|
|||
|
|
@ -171,6 +171,70 @@ export const ScrapedInputGroupRow: React.FC<IScrapedInputGroupRowProps> = (
|
|||
);
|
||||
};
|
||||
|
||||
interface IScrapedNumberInputProps {
|
||||
isNew?: boolean;
|
||||
placeholder?: string;
|
||||
locked?: boolean;
|
||||
result: ScrapeResult<number>;
|
||||
onChange?: (value: number) => void;
|
||||
}
|
||||
|
||||
const ScrapedNumberInput: React.FC<IScrapedNumberInputProps> = (props) => {
|
||||
return (
|
||||
<FormControl
|
||||
placeholder={props.placeholder}
|
||||
value={props.isNew ? props.result.newValue : props.result.originalValue}
|
||||
readOnly={!props.isNew || props.locked}
|
||||
onChange={(e) => {
|
||||
if (props.isNew && props.onChange) {
|
||||
props.onChange(Number(e.target.value));
|
||||
}
|
||||
}}
|
||||
className="bg-secondary text-white border-secondary"
|
||||
type="number"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface IScrapedNumberRowProps {
|
||||
title: string;
|
||||
field: string;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
result: ScrapeResult<number>;
|
||||
locked?: boolean;
|
||||
onChange: (value: ScrapeResult<number>) => void;
|
||||
}
|
||||
|
||||
export const ScrapedNumberRow: React.FC<IScrapedNumberRowProps> = (props) => {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title={props.title}
|
||||
field={props.field}
|
||||
className={props.className}
|
||||
result={props.result}
|
||||
originalField={
|
||||
<ScrapedNumberInput
|
||||
placeholder={props.placeholder || props.title}
|
||||
result={props.result}
|
||||
/>
|
||||
}
|
||||
newField={
|
||||
<ScrapedNumberInput
|
||||
placeholder={props.placeholder || props.title}
|
||||
result={props.result}
|
||||
isNew
|
||||
locked={props.locked}
|
||||
onChange={(value) =>
|
||||
props.onChange(props.result.cloneWithValue(value))
|
||||
}
|
||||
/>
|
||||
}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface IScrapedStringListProps {
|
||||
isNew?: boolean;
|
||||
placeholder?: string;
|
||||
|
|
|
|||
|
|
@ -240,7 +240,8 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
|||
height_cm: Number.parseFloat(performer.height ?? "") ?? undefined,
|
||||
measurements: performer.measurements,
|
||||
fake_tits: performer.fake_tits,
|
||||
career_length: performer.career_length,
|
||||
career_start: performer.career_start,
|
||||
career_end: performer.career_end,
|
||||
tattoos: performer.tattoos,
|
||||
piercings: performer.piercings,
|
||||
urls: performer.urls,
|
||||
|
|
@ -326,7 +327,8 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
|||
{maybeRenderField("measurements", performer.measurements)}
|
||||
{performer?.gender !== GQL.GenderEnum.Male &&
|
||||
maybeRenderField("fake_tits", performer.fake_tits)}
|
||||
{maybeRenderField("career_length", performer.career_length)}
|
||||
{maybeRenderField("career_start", performer.career_start?.toString())}
|
||||
{maybeRenderField("career_end", performer.career_end?.toString())}
|
||||
{maybeRenderField("tattoos", performer.tattoos, false)}
|
||||
{maybeRenderField("piercings", performer.piercings, false)}
|
||||
{maybeRenderField("weight", performer.weight, false)}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@ export const PERFORMER_FIELDS = [
|
|||
"fake_tits",
|
||||
"tattoos",
|
||||
"piercings",
|
||||
"career_length",
|
||||
"career_start",
|
||||
"career_end",
|
||||
"urls",
|
||||
"details",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -104,7 +104,10 @@ export const scrapedPerformerToCreateInput = (
|
|||
height_cm: toCreate.height ? Number(toCreate.height) : undefined,
|
||||
measurements: toCreate.measurements,
|
||||
fake_tits: toCreate.fake_tits,
|
||||
career_length: toCreate.career_length,
|
||||
career_start: toCreate.career_start
|
||||
? Number(toCreate.career_start)
|
||||
: undefined,
|
||||
career_end: toCreate.career_end ? Number(toCreate.career_end) : undefined,
|
||||
tattoos: toCreate.tattoos,
|
||||
piercings: toCreate.piercings,
|
||||
alias_list: aliases,
|
||||
|
|
|
|||
|
|
@ -175,7 +175,9 @@
|
|||
"filesystem": "Filesystem"
|
||||
},
|
||||
"captions": "Captions",
|
||||
"career_end": "Career End",
|
||||
"career_length": "Career Length",
|
||||
"career_start": "Career Start",
|
||||
"chapters": "Chapters",
|
||||
"circumcised": "Circumcised",
|
||||
"circumcised_types": {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ export const PerformerIsMissingCriterionOption = new IsMissingCriterionOption(
|
|||
"weight",
|
||||
"measurements",
|
||||
"fake_tits",
|
||||
"career_length",
|
||||
"career_start",
|
||||
"career_end",
|
||||
"tattoos",
|
||||
"piercings",
|
||||
"aliases",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ const sortByOptions = [
|
|||
"play_count",
|
||||
"last_played_at",
|
||||
"latest_scene",
|
||||
"career_length",
|
||||
"career_start",
|
||||
"career_end",
|
||||
"weight",
|
||||
"measurements",
|
||||
"scenes_duration",
|
||||
|
|
@ -75,6 +76,8 @@ const numberCriteria: CriterionType[] = [
|
|||
"age",
|
||||
"weight",
|
||||
"penis_length",
|
||||
"career_start",
|
||||
"career_end",
|
||||
];
|
||||
|
||||
const stringCriteria: CriterionType[] = [
|
||||
|
|
@ -86,7 +89,6 @@ const stringCriteria: CriterionType[] = [
|
|||
"eye_color",
|
||||
"measurements",
|
||||
"fake_tits",
|
||||
"career_length",
|
||||
"tattoos",
|
||||
"piercings",
|
||||
"aliases",
|
||||
|
|
|
|||
|
|
@ -166,6 +166,8 @@ export type CriterionType =
|
|||
| "penis_length"
|
||||
| "circumcised"
|
||||
| "career_length"
|
||||
| "career_start"
|
||||
| "career_end"
|
||||
| "tattoos"
|
||||
| "piercings"
|
||||
| "aliases"
|
||||
|
|
|
|||
Loading…
Reference in a new issue