From d2743cf5fb6790f9c79db2bb6d31a106fdae6e3c Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 8 Nov 2022 14:09:03 +1100 Subject: [PATCH] Change performer height to be numeric (#3060) * Make height an int. Add height_cm field * Change UI to use height_cm * Use number fields for height/weight * Add migration note --- graphql/documents/data/performer-slim.graphql | 2 +- graphql/documents/data/performer.graphql | 2 +- graphql/schema/types/filters.graphql | 4 +- graphql/schema/types/performer.graphql | 15 ++- internal/api/resolver_model_performer.go | 13 +++ internal/api/resolver_mutation_performer.go | 33 +++++- internal/identify/performer.go | 11 +- internal/identify/performer_test.go | 17 ++- internal/manager/task_stash_box_tag.go | 28 ++++- pkg/models/filter.go | 18 +++ pkg/models/jsonschema/performer.go | 19 ++-- pkg/models/model_performer.go | 4 +- pkg/models/performer.go | 4 +- pkg/performer/export.go | 7 +- pkg/performer/export_test.go | 7 +- pkg/performer/import.go | 12 +- pkg/scraper/stashbox/stash_box.go | 5 +- pkg/sqlite/database.go | 2 +- .../migrations/39_performer_height.up.sql | 103 ++++++++++++++++++ pkg/sqlite/performer.go | 39 ++++++- pkg/sqlite/performer_test.go | 83 +++++++++----- pkg/sqlite/sql.go | 2 +- .../PerformerDetailsPanel.tsx | 6 +- .../PerformerDetails/PerformerEditPanel.tsx | 59 ++++++---- .../PerformerScrapeDialog.tsx | 5 +- .../Performers/PerformerListTable.tsx | 2 +- .../src/components/Tagger/PerformerModal.tsx | 6 +- ui/v2.5/src/core/StashService.ts | 2 +- ui/v2.5/src/docs/en/Changelog/v0180.md | 6 + ui/v2.5/src/docs/en/MigrationNotes/38.md | 1 + ui/v2.5/src/docs/en/MigrationNotes/index.ts | 2 + ui/v2.5/src/locales/en-GB.json | 2 + .../models/list-filter/criteria/factory.ts | 6 +- ui/v2.5/src/models/list-filter/performers.ts | 3 +- ui/v2.5/src/models/list-filter/types.ts | 1 + 35 files changed, 432 insertions(+), 99 deletions(-) create mode 100644 pkg/sqlite/migrations/39_performer_height.up.sql create mode 100644 ui/v2.5/src/docs/en/MigrationNotes/38.md diff --git a/graphql/documents/data/performer-slim.graphql b/graphql/documents/data/performer-slim.graphql index 1dd692a6e..62d1b9b7a 100644 --- a/graphql/documents/data/performer-slim.graphql +++ b/graphql/documents/data/performer-slim.graphql @@ -13,7 +13,7 @@ fragment SlimPerformerData on Performer { ethnicity hair_color eye_color - height + height_cm fake_tits career_length tattoos diff --git a/graphql/documents/data/performer.graphql b/graphql/documents/data/performer.graphql index 4030d6697..2c27fb6cc 100644 --- a/graphql/documents/data/performer.graphql +++ b/graphql/documents/data/performer.graphql @@ -10,7 +10,7 @@ fragment PerformerData on Performer { ethnicity country eye_color - height + height_cm measurements fake_tits career_length diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index 777b7dcf8..f60f37a69 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -60,7 +60,9 @@ input PerformerFilterType { """Filter by eye color""" eye_color: StringCriterionInput """Filter by height""" - height: StringCriterionInput + height: StringCriterionInput @deprecated(reason: "Use height_cm instead") + """Filter by height in cm""" + height_cm: IntCriterionInput """Filter by measurements""" measurements: StringCriterionInput """Filter by fake tits value""" diff --git a/graphql/schema/types/performer.graphql b/graphql/schema/types/performer.graphql index e69d52e47..1a2002610 100644 --- a/graphql/schema/types/performer.graphql +++ b/graphql/schema/types/performer.graphql @@ -19,7 +19,8 @@ type Performer { ethnicity: String country: String eye_color: String - height: String + height: String @deprecated(reason: "Use height_cm") + height_cm: Int measurements: String fake_tits: String career_length: String @@ -55,7 +56,9 @@ input PerformerCreateInput { ethnicity: String country: String eye_color: String - height: String + # height must be parsable into an integer + height: String @deprecated(reason: "Use height_cm") + height_cm: Int measurements: String fake_tits: String career_length: String @@ -86,7 +89,9 @@ input PerformerUpdateInput { ethnicity: String country: String eye_color: String - height: String + # height must be parsable into an integer + height: String @deprecated(reason: "Use height_cm") + height_cm: Int measurements: String fake_tits: String career_length: String @@ -117,7 +122,9 @@ input BulkPerformerUpdateInput { ethnicity: String country: String eye_color: String - height: String + # height must be parsable into an integer + height: String @deprecated(reason: "Use height_cm") + height_cm: Int measurements: String fake_tits: String career_length: String diff --git a/internal/api/resolver_model_performer.go b/internal/api/resolver_model_performer.go index ba7226d10..f80cd4c35 100644 --- a/internal/api/resolver_model_performer.go +++ b/internal/api/resolver_model_performer.go @@ -2,6 +2,7 @@ package api import ( "context" + "strconv" "github.com/stashapp/stash/internal/api/urlbuilders" "github.com/stashapp/stash/pkg/gallery" @@ -9,6 +10,18 @@ import ( "github.com/stashapp/stash/pkg/models" ) +func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) { + if obj.Height != nil { + ret := strconv.Itoa(*obj.Height) + return &ret, nil + } + return nil, nil +} + +func (r *performerResolver) HeightCm(ctx context.Context, obj *models.Performer) (*int, error) { + return obj.Height, nil +} + func (r *performerResolver) Birthdate(ctx context.Context, obj *models.Performer) (*string, error) { if obj.Birthdate != nil { ret := obj.Birthdate.String() diff --git a/internal/api/resolver_mutation_performer.go b/internal/api/resolver_mutation_performer.go index 7ce3f624f..15c6610a8 100644 --- a/internal/api/resolver_mutation_performer.go +++ b/internal/api/resolver_mutation_performer.go @@ -77,8 +77,15 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC if input.EyeColor != nil { newPerformer.EyeColor = *input.EyeColor } - if input.Height != nil { - newPerformer.Height = *input.Height + // prefer height_cm over height + if input.HeightCm != nil { + newPerformer.Height = input.HeightCm + } else if input.Height != nil { + h, err := strconv.Atoi(*input.Height) + if err != nil { + return nil, fmt.Errorf("invalid height: %s", *input.Height) + } + newPerformer.Height = &h } if input.Measurements != nil { newPerformer.Measurements = *input.Measurements @@ -213,7 +220,16 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU updatedPerformer.Country = translator.optionalString(input.Country, "country") updatedPerformer.EyeColor = translator.optionalString(input.EyeColor, "eye_color") updatedPerformer.Measurements = translator.optionalString(input.Measurements, "measurements") - updatedPerformer.Height = translator.optionalString(input.Height, "height") + // prefer height_cm over height + if translator.hasField("height_cm") { + updatedPerformer.Height = translator.optionalInt(input.HeightCm, "height_cm") + } else if translator.hasField("height") { + updatedPerformer.Height, err = translator.optionalIntFromString(input.Height, "height") + if err != nil { + return nil, err + } + } + updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity") updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits") updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length") @@ -317,7 +333,16 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity") updatedPerformer.Country = translator.optionalString(input.Country, "country") updatedPerformer.EyeColor = translator.optionalString(input.EyeColor, "eye_color") - updatedPerformer.Height = translator.optionalString(input.Height, "height") + // prefer height_cm over height + if translator.hasField("height_cm") { + updatedPerformer.Height = translator.optionalInt(input.HeightCm, "height_cm") + } else if translator.hasField("height") { + updatedPerformer.Height, err = translator.optionalIntFromString(input.Height, "height") + if err != nil { + return nil, err + } + } + updatedPerformer.Measurements = translator.optionalString(input.Measurements, "measurements") updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits") updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length") diff --git a/internal/identify/performer.go b/internal/identify/performer.go index 97621502f..d417d8bac 100644 --- a/internal/identify/performer.go +++ b/internal/identify/performer.go @@ -84,7 +84,16 @@ func scrapedToPerformerInput(performer *models.ScrapedPerformer) models.Performe ret.HairColor = *performer.HairColor } if performer.Height != nil { - ret.Height = *performer.Height + h, err := strconv.Atoi(*performer.Height) // height is stored as an int + if err == nil { + ret.Height = &h + } + } + if performer.Weight != nil { + h, err := strconv.Atoi(*performer.Weight) + if err == nil { + ret.Weight = &h + } } if performer.Measurements != nil { ret.Measurements = *performer.Measurements diff --git a/internal/identify/performer_test.go b/internal/identify/performer_test.go index df18b1d09..764b4ec79 100644 --- a/internal/identify/performer_test.go +++ b/internal/identify/performer_test.go @@ -10,6 +10,7 @@ import ( "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/mocks" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -233,7 +234,7 @@ func Test_scrapedToPerformerInput(t *testing.T) { md5 := "b068931cc450442b63f5b3d276ea4297" var stringValues []string - for i := 0; i < 16; i++ { + for i := 0; i < 17; i++ { stringValues = append(stringValues, strconv.Itoa(i)) } @@ -244,6 +245,12 @@ func Test_scrapedToPerformerInput(t *testing.T) { return &ret } + nextIntVal := func() *int { + ret := upTo + upTo = (upTo + 1) % len(stringValues) + return &ret + } + dateToDatePtr := func(d models.Date) *models.Date { return &d } @@ -265,6 +272,7 @@ func Test_scrapedToPerformerInput(t *testing.T) { EyeColor: nextVal(), HairColor: nextVal(), Height: nextVal(), + Weight: nextVal(), Measurements: nextVal(), FakeTits: nextVal(), CareerLength: nextVal(), @@ -284,7 +292,8 @@ func Test_scrapedToPerformerInput(t *testing.T) { Country: *nextVal(), EyeColor: *nextVal(), HairColor: *nextVal(), - Height: *nextVal(), + Height: nextIntVal(), + Weight: nextIntVal(), Measurements: *nextVal(), FakeTits: *nextVal(), CareerLength: *nextVal(), @@ -314,9 +323,7 @@ func Test_scrapedToPerformerInput(t *testing.T) { got.CreatedAt = time.Time{} got.UpdatedAt = got.CreatedAt - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("scrapedToPerformerInput() = %v, want %v", got, tt.want) - } + assert.Equal(t, tt.want, got) }) } } diff --git a/internal/manager/task_stash_box_tag.go b/internal/manager/task_stash_box_tag.go index 36d3a3207..33b26d689 100644 --- a/internal/manager/task_stash_box_tag.go +++ b/internal/manager/task_stash_box_tag.go @@ -3,6 +3,7 @@ package manager import ( "context" "fmt" + "strconv" "time" "github.com/stashapp/stash/pkg/hash/md5" @@ -114,7 +115,16 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { partial.Gender = models.NewOptionalString(*performer.Gender) } if performer.Height != nil && !excluded["height"] { - partial.Height = models.NewOptionalString(*performer.Height) + h, err := strconv.Atoi(*performer.Height) + if err == nil { + partial.Height = models.NewOptionalInt(h) + } + } + if performer.Weight != nil && !excluded["weight"] { + w, err := strconv.Atoi(*performer.Weight) + if err == nil { + partial.Weight = models.NewOptionalInt(w) + } } if performer.Instagram != nil && !excluded["instagram"] { partial.Instagram = models.NewOptionalString(*performer.Instagram) @@ -192,7 +202,8 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { EyeColor: getString(performer.EyeColor), FakeTits: getString(performer.FakeTits), Gender: models.GenderEnum(getString(performer.Gender)), - Height: getString(performer.Height), + Height: getIntPtr(performer.Height), + Weight: getIntPtr(performer.Weight), Instagram: getString(performer.Instagram), Measurements: getString(performer.Measurements), Name: *performer.Name, @@ -261,3 +272,16 @@ func getString(val *string) string { return *val } } + +func getIntPtr(val *string) *int { + if val == nil { + return nil + } else { + v, err := strconv.Atoi(*val) + if err != nil { + return nil + } + + return &v + } +} diff --git a/pkg/models/filter.go b/pkg/models/filter.go index 57bee72df..ad5db4282 100644 --- a/pkg/models/filter.go +++ b/pkg/models/filter.go @@ -85,12 +85,30 @@ type StringCriterionInput struct { Modifier CriterionModifier `json:"modifier"` } +func (i StringCriterionInput) ValidModifier() bool { + switch i.Modifier { + case CriterionModifierEquals, CriterionModifierNotEquals, CriterionModifierIncludes, CriterionModifierExcludes, CriterionModifierMatchesRegex, CriterionModifierNotMatchesRegex, + CriterionModifierIsNull, CriterionModifierNotNull: + return true + } + + return false +} + type IntCriterionInput struct { Value int `json:"value"` Value2 *int `json:"value2"` Modifier CriterionModifier `json:"modifier"` } +func (i IntCriterionInput) ValidModifier() bool { + switch i.Modifier { + case CriterionModifierEquals, CriterionModifierNotEquals, CriterionModifierGreaterThan, CriterionModifierLessThan, CriterionModifierIsNull, CriterionModifierNotNull, CriterionModifierBetween, CriterionModifierNotBetween: + return true + } + return false +} + type ResolutionCriterionInput struct { Value ResolutionEnum `json:"value"` Modifier CriterionModifier `json:"modifier"` diff --git a/pkg/models/jsonschema/performer.go b/pkg/models/jsonschema/performer.go index ad33452f3..e4f5de2cb 100644 --- a/pkg/models/jsonschema/performer.go +++ b/pkg/models/jsonschema/performer.go @@ -11,15 +11,16 @@ import ( ) 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"` - Birthdate string `json:"birthdate,omitempty"` - Ethnicity string `json:"ethnicity,omitempty"` - Country string `json:"country,omitempty"` - EyeColor string `json:"eye_color,omitempty"` + 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"` + Birthdate string `json:"birthdate,omitempty"` + Ethnicity string `json:"ethnicity,omitempty"` + Country string `json:"country,omitempty"` + EyeColor string `json:"eye_color,omitempty"` + // this should be int, but keeping string for backwards compatibility Height string `json:"height,omitempty"` Measurements string `json:"measurements,omitempty"` FakeTits string `json:"fake_tits,omitempty"` diff --git a/pkg/models/model_performer.go b/pkg/models/model_performer.go index f8b7d9597..b6c9eff4d 100644 --- a/pkg/models/model_performer.go +++ b/pkg/models/model_performer.go @@ -18,7 +18,7 @@ type Performer struct { Ethnicity string `json:"ethnicity"` Country string `json:"country"` EyeColor string `json:"eye_color"` - Height string `json:"height"` + Height *int `json:"height"` Measurements string `json:"measurements"` FakeTits string `json:"fake_tits"` CareerLength string `json:"career_length"` @@ -50,7 +50,7 @@ type PerformerPartial struct { Ethnicity OptionalString Country OptionalString EyeColor OptionalString - Height OptionalString + Height OptionalInt Measurements OptionalString FakeTits OptionalString CareerLength OptionalString diff --git a/pkg/models/performer.go b/pkg/models/performer.go index 924ffb205..191f6b4b9 100644 --- a/pkg/models/performer.go +++ b/pkg/models/performer.go @@ -79,8 +79,10 @@ type PerformerFilterType struct { Country *StringCriterionInput `json:"country"` // Filter by eye color EyeColor *StringCriterionInput `json:"eye_color"` - // Filter by height + // Filter by height - deprecated: use height_cm instead Height *StringCriterionInput `json:"height"` + // Filter by height in centimeters + HeightCm *IntCriterionInput `json:"height_cm"` // Filter by measurements Measurements *StringCriterionInput `json:"measurements"` // Filter by fake tits value diff --git a/pkg/performer/export.go b/pkg/performer/export.go index e366a0393..90e50cb69 100644 --- a/pkg/performer/export.go +++ b/pkg/performer/export.go @@ -3,6 +3,7 @@ package performer import ( "context" "fmt" + "strconv" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/json" @@ -24,7 +25,6 @@ func ToJSON(ctx context.Context, reader ImageStashIDGetter, performer *models.Pe Ethnicity: performer.Ethnicity, Country: performer.Country, EyeColor: performer.EyeColor, - Height: performer.Height, Measurements: performer.Measurements, FakeTits: performer.FakeTits, CareerLength: performer.CareerLength, @@ -50,6 +50,11 @@ func ToJSON(ctx context.Context, reader ImageStashIDGetter, performer *models.Pe if performer.DeathDate != nil { newPerformerJSON.DeathDate = performer.DeathDate.String() } + + if performer.Height != nil { + newPerformerJSON.Height = strconv.Itoa(*performer.Height) + } + if performer.Weight != nil { newPerformerJSON.Weight = *performer.Weight } diff --git a/pkg/performer/export_test.go b/pkg/performer/export_test.go index 2271b6cbc..d3ee15d46 100644 --- a/pkg/performer/export_test.go +++ b/pkg/performer/export_test.go @@ -2,6 +2,7 @@ package performer import ( "errors" + "strconv" "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/models" @@ -30,7 +31,6 @@ const ( eyeColor = "eyeColor" fakeTits = "fakeTits" gender = "gender" - height = "height" instagram = "instagram" measurements = "measurements" piercings = "piercings" @@ -44,6 +44,7 @@ const ( var ( rating = 5 + height = 123 weight = 60 ) @@ -82,7 +83,7 @@ func createFullPerformer(id int, name string) *models.Performer { FakeTits: fakeTits, Favorite: true, Gender: gender, - Height: height, + Height: &height, Instagram: instagram, Measurements: measurements, Piercings: piercings, @@ -120,7 +121,7 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer { FakeTits: fakeTits, Favorite: true, Gender: gender, - Height: height, + Height: strconv.Itoa(height), Instagram: instagram, Measurements: measurements, Piercings: piercings, diff --git a/pkg/performer/import.go b/pkg/performer/import.go index 05b4c4b2c..62c1d1b95 100644 --- a/pkg/performer/import.go +++ b/pkg/performer/import.go @@ -3,9 +3,11 @@ package performer import ( "context" "fmt" + "strconv" "strings" "github.com/stashapp/stash/pkg/hash/md5" + "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/jsonschema" "github.com/stashapp/stash/pkg/sliceutil/stringslice" @@ -194,7 +196,6 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform Ethnicity: performerJSON.Ethnicity, Country: performerJSON.Country, EyeColor: performerJSON.EyeColor, - Height: performerJSON.Height, Measurements: performerJSON.Measurements, FakeTits: performerJSON.FakeTits, CareerLength: performerJSON.CareerLength, @@ -235,5 +236,14 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform newPerformer.Weight = &performerJSON.Weight } + if performerJSON.Height != "" { + h, err := strconv.Atoi(performerJSON.Height) + if err == nil { + newPerformer.Height = &h + } else { + logger.Warnf("error parsing height %q: %v", performerJSON.Height, err) + } + } + return newPerformer } diff --git a/pkg/scraper/stashbox/stash_box.go b/pkg/scraper/stashbox/stash_box.go index 8bac9a154..1f7820378 100644 --- a/pkg/scraper/stashbox/stash_box.go +++ b/pkg/scraper/stashbox/stash_box.go @@ -975,8 +975,9 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf if performer.HairColor != "" { draft.HairColor = &performer.HairColor } - if performer.Height != "" { - draft.Height = &performer.Height + if performer.Height != nil { + v := strconv.Itoa(*performer.Height) + draft.Height = &v } if performer.Measurements != "" { draft.Measurements = &performer.Measurements diff --git a/pkg/sqlite/database.go b/pkg/sqlite/database.go index 55b3f01d0..11de78540 100644 --- a/pkg/sqlite/database.go +++ b/pkg/sqlite/database.go @@ -22,7 +22,7 @@ import ( "github.com/stashapp/stash/pkg/logger" ) -var appSchemaVersion uint = 38 +var appSchemaVersion uint = 39 //go:embed migrations/*.sql var migrationsBox embed.FS diff --git a/pkg/sqlite/migrations/39_performer_height.up.sql b/pkg/sqlite/migrations/39_performer_height.up.sql new file mode 100644 index 000000000..4203405ce --- /dev/null +++ b/pkg/sqlite/migrations/39_performer_height.up.sql @@ -0,0 +1,103 @@ +-- add primary keys to association tables that are missing them +PRAGMA foreign_keys=OFF; + +CREATE TABLE `performers_new` ( + `id` integer not null primary key autoincrement, + `checksum` varchar(255) not null, + `name` varchar(255), + `gender` varchar(20), + `url` varchar(255), + `twitter` varchar(255), + `instagram` varchar(255), + `birthdate` date, + `ethnicity` varchar(255), + `country` varchar(255), + `eye_color` varchar(255), + -- changed from varchar(255) + `height` int, + `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, + `details` text, + `death_date` date, + `hair_color` varchar(255), + `weight` integer, + `rating` tinyint, + `ignore_auto_tag` boolean not null default '0' +); + +INSERT INTO `performers_new` + ( + `id`, + `checksum`, + `name`, + `gender`, + `url`, + `twitter`, + `instagram`, + `birthdate`, + `ethnicity`, + `country`, + `eye_color`, + `height`, + `measurements`, + `fake_tits`, + `career_length`, + `tattoos`, + `piercings`, + `aliases`, + `favorite`, + `created_at`, + `updated_at`, + `details`, + `death_date`, + `hair_color`, + `weight`, + `rating`, + `ignore_auto_tag` + ) + SELECT + `id`, + `checksum`, + `name`, + `gender`, + `url`, + `twitter`, + `instagram`, + `birthdate`, + `ethnicity`, + `country`, + `eye_color`, + CASE `height` + WHEN '' THEN NULL + WHEN NULL THEN NULL + ELSE CAST(`height` as int) + END, + `measurements`, + `fake_tits`, + `career_length`, + `tattoos`, + `piercings`, + `aliases`, + `favorite`, + `created_at`, + `updated_at`, + `details`, + `death_date`, + `hair_color`, + `weight`, + `rating`, + `ignore_auto_tag` + FROM `performers`; + +DROP TABLE `performers`; +ALTER TABLE `performers_new` rename to `performers`; + +CREATE UNIQUE INDEX `performers_checksum_unique` on `performers` (`checksum`); +CREATE INDEX `index_performers_on_name` on `performers` (`name`); diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index 3cf6fc15b..5c4d5367c 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "strconv" "strings" "github.com/doug-martin/goqu/v9" @@ -33,7 +34,7 @@ type performerRow struct { Ethnicity zero.String `db:"ethnicity"` Country zero.String `db:"country"` EyeColor zero.String `db:"eye_color"` - Height zero.String `db:"height"` + Height null.Int `db:"height"` Measurements zero.String `db:"measurements"` FakeTits zero.String `db:"fake_tits"` CareerLength zero.String `db:"career_length"` @@ -67,7 +68,7 @@ func (r *performerRow) fromPerformer(o models.Performer) { r.Ethnicity = zero.StringFrom(o.Ethnicity) r.Country = zero.StringFrom(o.Country) r.EyeColor = zero.StringFrom(o.EyeColor) - r.Height = zero.StringFrom(o.Height) + r.Height = intFromPtr(o.Height) r.Measurements = zero.StringFrom(o.Measurements) r.FakeTits = zero.StringFrom(o.FakeTits) r.CareerLength = zero.StringFrom(o.CareerLength) @@ -100,7 +101,7 @@ func (r *performerRow) resolve() *models.Performer { Ethnicity: r.Ethnicity.String, Country: r.Country.String, EyeColor: r.EyeColor.String, - Height: r.Height.String, + Height: nullIntPtr(r.Height), Measurements: r.Measurements.String, FakeTits: r.FakeTits.String, CareerLength: r.CareerLength.String, @@ -136,7 +137,7 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) { r.setNullString("ethnicity", o.Ethnicity) r.setNullString("country", o.Country) r.setNullString("eye_color", o.EyeColor) - r.setNullString("height", o.Height) + r.setNullInt("height", o.Height) r.setNullString("measurements", o.Measurements) r.setNullString("fake_tits", o.FakeTits) r.setNullString("career_length", o.CareerLength) @@ -445,6 +446,22 @@ func (qb *PerformerStore) validateFilter(filter *models.PerformerFilterType) err return qb.validateFilter(filter.Not) } + // if legacy height filter used, ensure only supported modifiers are used + if filter.Height != nil { + // treat as an int filter + intCrit := &models.IntCriterionInput{ + Modifier: filter.Height.Modifier, + } + if !intCrit.ValidModifier() { + return fmt.Errorf("invalid height modifier: %s", filter.Height.Modifier) + } + + // ensure value is a valid number + if _, err := strconv.Atoi(filter.Height.Value); err != nil { + return fmt.Errorf("invalid height value: %s", filter.Height.Value) + } + } + return nil } @@ -483,7 +500,19 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform query.handleCriterion(ctx, stringCriterionHandler(filter.Ethnicity, tableName+".ethnicity")) query.handleCriterion(ctx, stringCriterionHandler(filter.Country, tableName+".country")) query.handleCriterion(ctx, stringCriterionHandler(filter.EyeColor, tableName+".eye_color")) - query.handleCriterion(ctx, stringCriterionHandler(filter.Height, tableName+".height")) + + // special handler for legacy height filter + heightCmCrit := filter.HeightCm + if heightCmCrit == nil && filter.Height != nil { + heightCm, _ := strconv.Atoi(filter.Height.Value) // already validated + heightCmCrit = &models.IntCriterionInput{ + Value: heightCm, + Modifier: filter.Height.Modifier, + } + } + + query.handleCriterion(ctx, intCriterionHandler(heightCmCrit, tableName+".height", nil)) + query.handleCriterion(ctx, stringCriterionHandler(filter.Measurements, tableName+".measurements")) query.handleCriterion(ctx, stringCriterionHandler(filter.FakeTits, tableName+".fake_tits")) query.handleCriterion(ctx, stringCriterionHandler(filter.CareerLength, tableName+".career_length")) diff --git a/pkg/sqlite/performer_test.go b/pkg/sqlite/performer_test.go index 40736f617..2b089a3ef 100644 --- a/pkg/sqlite/performer_test.go +++ b/pkg/sqlite/performer_test.go @@ -30,7 +30,7 @@ func Test_PerformerStore_Update(t *testing.T) { ethnicity = "ethnicity" country = "country" eyeColor = "eyeColor" - height = "height" + height = 134 measurements = "measurements" fakeTits = "fakeTits" careerLength = "careerLength" @@ -67,7 +67,7 @@ func Test_PerformerStore_Update(t *testing.T) { Ethnicity: ethnicity, Country: country, EyeColor: eyeColor, - Height: height, + Height: &height, Measurements: measurements, FakeTits: fakeTits, CareerLength: careerLength, @@ -133,7 +133,7 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { ethnicity = "ethnicity" country = "country" eyeColor = "eyeColor" - height = "height" + height = 143 measurements = "measurements" fakeTits = "fakeTits" careerLength = "careerLength" @@ -172,7 +172,7 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { Ethnicity: models.NewOptionalString(ethnicity), Country: models.NewOptionalString(country), EyeColor: models.NewOptionalString(eyeColor), - Height: models.NewOptionalString(height), + Height: models.NewOptionalInt(height), Measurements: models.NewOptionalString(measurements), FakeTits: models.NewOptionalString(fakeTits), CareerLength: models.NewOptionalString(careerLength), @@ -201,7 +201,7 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { Ethnicity: ethnicity, Country: country, EyeColor: eyeColor, - Height: height, + Height: &height, Measurements: measurements, FakeTits: fakeTits, CareerLength: careerLength, @@ -506,29 +506,62 @@ func TestPerformerIllegalQuery(t *testing.T) { }, } - performerFilter := &models.PerformerFilterType{ - And: &subFilter, - Or: &subFilter, + tests := []struct { + name string + filter models.PerformerFilterType + }{ + { + // And and Or in the same filter + "AndOr", + models.PerformerFilterType{ + And: &subFilter, + Or: &subFilter, + }, + }, + { + // And and Not in the same filter + "AndNot", + models.PerformerFilterType{ + And: &subFilter, + Not: &subFilter, + }, + }, + { + // Or and Not in the same filter + "OrNot", + models.PerformerFilterType{ + Or: &subFilter, + Not: &subFilter, + }, + }, + { + "invalid height modifier", + models.PerformerFilterType{ + Height: &models.StringCriterionInput{ + Modifier: models.CriterionModifierMatchesRegex, + Value: "123", + }, + }, + }, + { + "invalid height value", + models.PerformerFilterType{ + Height: &models.StringCriterionInput{ + Modifier: models.CriterionModifierEquals, + Value: "foo", + }, + }, + }, } - withTxn(func(ctx context.Context) error { - sqb := db.Performer + sqb := db.Performer - _, _, err := sqb.Query(ctx, performerFilter, nil) - assert.NotNil(err) - - performerFilter.Or = nil - performerFilter.Not = &subFilter - _, _, err = sqb.Query(ctx, performerFilter, nil) - assert.NotNil(err) - - performerFilter.And = nil - performerFilter.Or = &subFilter - _, _, err = sqb.Query(ctx, performerFilter, nil) - assert.NotNil(err) - - return nil - }) + for _, tt := range tests { + runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { + _, _, err := sqb.Query(ctx, &tt.filter, nil) + assert.NotNil(err) + }) + } } func TestPerformerQueryIgnoreAutoTag(t *testing.T) { diff --git a/pkg/sqlite/sql.go b/pkg/sqlite/sql.go index e80cceef8..f060c574b 100644 --- a/pkg/sqlite/sql.go +++ b/pkg/sqlite/sql.go @@ -178,7 +178,7 @@ func getIntWhereClause(column string, modifier models.CriterionModifier, value i return fmt.Sprintf("%s > ?", column), args } - panic("unsupported int modifier type") + panic("unsupported int modifier type " + modifier) } // returns where clause and having clause diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx index a71729a04..52211ea0e 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -71,11 +71,11 @@ export const PerformerDetailsPanel: React.FC = ({ ); } - const formatHeight = (height?: string | null) => { + const formatHeight = (height?: number | null) => { if (!height) { return ""; } - return intl.formatNumber(Number.parseInt(height, 10), { + return intl.formatNumber(height, { style: "unit", unit: "centimeter", unitDisplay: "narrow", @@ -120,7 +120,7 @@ export const PerformerDetailsPanel: React.FC = ({ getCountryByISO(performer.country, intl.locale) ?? performer.country } /> - + diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index b9297ac99..752cb4e48 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -106,7 +106,7 @@ export const PerformerEditPanel: React.FC = ({ ethnicity: yup.string().optional(), eye_color: yup.string().optional(), country: yup.string().optional(), - height: yup.string().optional(), + height_cm: yup.number().optional(), measurements: yup.string().optional(), fake_tits: yup.string().optional(), career_length: yup.string().optional(), @@ -133,7 +133,7 @@ export const PerformerEditPanel: React.FC = ({ ethnicity: performer.ethnicity ?? "", eye_color: performer.eye_color ?? "", country: performer.country ?? "", - height: performer.height ?? "", + height_cm: performer.height_cm ?? undefined, measurements: performer.measurements ?? "", fake_tits: performer.fake_tits ?? "", career_length: performer.career_length ?? "", @@ -279,7 +279,7 @@ export const PerformerEditPanel: React.FC = ({ formik.setFieldValue("eye_color", state.eye_color); } if (state.height) { - formik.setFieldValue("height", state.height); + formik.setFieldValue("height_cm", parseInt(state.height, 10)); } if (state.measurements) { formik.setFieldValue("measurements", state.measurements); @@ -445,7 +445,8 @@ export const PerformerEditPanel: React.FC = ({ return { ...values, gender: stringToGender(values.gender) ?? null, - weight: Number(values.weight), + height_cm: values.height_cm ? Number(values.height_cm) : null, + weight: values.weight ? Number(values.weight) : null, id: performer.id ?? "", }; } @@ -454,7 +455,8 @@ export const PerformerEditPanel: React.FC = ({ return { ...values, gender: stringToGender(values.gender), - weight: Number(values.weight), + height_cm: values.height_cm ? Number(values.height_cm) : null, + weight: values.weight ? Number(values.weight) : null, }; } @@ -797,16 +799,26 @@ export const PerformerEditPanel: React.FC = ({ ); } - function renderTextField(field: string, title: string, placeholder?: string) { + function renderField( + field: string, + props?: { + messageID?: string; + placeholder?: string; + type?: string; + } + ) { + const title = intl.formatMessage({ id: props?.messageID ?? field }); + return ( - + {title} @@ -877,8 +889,8 @@ export const PerformerEditPanel: React.FC = ({ - {renderTextField("birthdate", "Birthdate", "YYYY-MM-DD")} - {renderTextField("death_date", "Death Date", "YYYY-MM-DD")} + {renderField("birthdate", { placeholder: "YYYY-MM-DD" })} + {renderField("death_date", { placeholder: "YYYY-MM-DD" })} @@ -892,13 +904,20 @@ export const PerformerEditPanel: React.FC = ({ - {renderTextField("ethnicity", "Ethnicity")} - {renderTextField("hair_color", "Hair Color")} - {renderTextField("eye_color", "Eye Color")} - {renderTextField("height", "Height (cm)")} - {renderTextField("weight", "Weight (kg)")} - {renderTextField("measurements", "Measurements")} - {renderTextField("fake_tits", "Fake Tits")} + {renderField("ethnicity")} + {renderField("hair_color")} + {renderField("eye_color")} + {renderField("height_cm", { + type: "number", + messageID: "height", + placeholder: intl.formatMessage({ id: "height_cm" }), + })} + {renderField("weight", { + type: "number", + placeholder: intl.formatMessage({ id: "weight_kg" }), + })} + {renderField("measurements")} + {renderField("fake_tits")} @@ -928,7 +947,7 @@ export const PerformerEditPanel: React.FC = ({ - {renderTextField("career_length", "Career Length")} + {renderField("career_length")} @@ -943,8 +962,8 @@ export const PerformerEditPanel: React.FC = ({ - {renderTextField("twitter", "Twitter")} - {renderTextField("instagram", "Instagram")} + {renderField("twitter")} + {renderField("instagram")} diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx index 963beb38e..8cca0c4b6 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx @@ -196,7 +196,10 @@ export const PerformerScrapeDialog: React.FC = ( new ScrapeResult(props.performer.eye_color, props.scraped.eye_color) ); const [height, setHeight] = useState>( - new ScrapeResult(props.performer.height, props.scraped.height) + new ScrapeResult( + props.performer.height_cm?.toString(), + props.scraped.height + ) ); const [weight, setWeight] = useState>( new ScrapeResult( diff --git a/ui/v2.5/src/components/Performers/PerformerListTable.tsx b/ui/v2.5/src/components/Performers/PerformerListTable.tsx index 36af0e841..1a21af4b3 100644 --- a/ui/v2.5/src/components/Performers/PerformerListTable.tsx +++ b/ui/v2.5/src/components/Performers/PerformerListTable.tsx @@ -58,7 +58,7 @@ export const PerformerListTable: React.FC = ( {performer.birthdate} - {performer.height} + {performer.height_cm} ); diff --git a/ui/v2.5/src/components/Tagger/PerformerModal.tsx b/ui/v2.5/src/components/Tagger/PerformerModal.tsx index e1613d2f5..0f13b6ba4 100755 --- a/ui/v2.5/src/components/Tagger/PerformerModal.tsx +++ b/ui/v2.5/src/components/Tagger/PerformerModal.tsx @@ -133,7 +133,7 @@ const PerformerModal: React.FC = ({ ethnicity: performer.ethnicity, eye_color: performer.eye_color, country: performer.country, - height: performer.height, + height_cm: Number.parseFloat(performer.height ?? "") ?? undefined, measurements: performer.measurements, fake_tits: performer.fake_tits, career_length: performer.career_length, @@ -153,6 +153,10 @@ const PerformerModal: React.FC = ({ performerData.weight = undefined; } + if (Number.isNaN(performerData.height ?? 0)) { + performerData.height = undefined; + } + if (performer.tags) { performerData.tag_ids = performer.tags .map((t) => t.stored_id) diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index 1f42a3ffd..506f2ea88 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -1194,7 +1194,7 @@ export const makePerformerCreateInput = (toCreate: GQL.ScrapedPerformer) => { ethnicity: toCreate.ethnicity, country: toCreate.country, eye_color: toCreate.eye_color, - height: toCreate.height, + height_cm: toCreate.height ? Number(toCreate.height) : undefined, measurements: toCreate.measurements, fake_tits: toCreate.fake_tits, career_length: toCreate.career_length, diff --git a/ui/v2.5/src/docs/en/Changelog/v0180.md b/ui/v2.5/src/docs/en/Changelog/v0180.md index 88d00e493..ccaff7ff1 100644 --- a/ui/v2.5/src/docs/en/Changelog/v0180.md +++ b/ui/v2.5/src/docs/en/Changelog/v0180.md @@ -3,5 +3,11 @@ * Added selector for Country field. ([#1922](https://github.com/stashapp/stash/pull/1922)) * Added tag description filter criterion. ([#3011](https://github.com/stashapp/stash/pull/3011)) +### 🎨 Improvements +* Changed Performer height to be numeric, and changed filtering accordingly. ((#3060)[https://github.com/stashapp/stash/pull/3060]) + ### 🐛 Bug fixes +* Scene Player no longer always resumes playing when seeking. ([#3020](https://github.com/stashapp/stash/pull/3020)) +* Fixed space bar sometimes no playing/pausing the scene player. ([#3020](https://github.com/stashapp/stash/pull/3020)) +* Fixed scrubber thumbnails not disappearing when seeking on mobile. ([#3020](https://github.com/stashapp/stash/pull/3020)) * Fix path filter behaviour to be consistent with previous behaviour. ([#3041](https://github.com/stashapp/stash/pull/3041)) diff --git a/ui/v2.5/src/docs/en/MigrationNotes/38.md b/ui/v2.5/src/docs/en/MigrationNotes/38.md new file mode 100644 index 000000000..8e12a3153 --- /dev/null +++ b/ui/v2.5/src/docs/en/MigrationNotes/38.md @@ -0,0 +1 @@ +This migration changes performer height values from strings to numbers. Non-numeric performer height values **will be erased during this migration**. \ No newline at end of file diff --git a/ui/v2.5/src/docs/en/MigrationNotes/index.ts b/ui/v2.5/src/docs/en/MigrationNotes/index.ts index df11b7bf2..ead5b9ff7 100644 --- a/ui/v2.5/src/docs/en/MigrationNotes/index.ts +++ b/ui/v2.5/src/docs/en/MigrationNotes/index.ts @@ -1,7 +1,9 @@ import migration32 from "./32.md"; +import migration38 from "./38.md"; type Module = typeof migration32; export const migrationNotes: Record = { 32: migration32, + 38: migration38, }; diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 3ba8f4bd6..704ae11c8 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -807,6 +807,7 @@ }, "hasMarkers": "Has Markers", "height": "Height", + "height_cm": "Height (cm)", "help": "Help", "ignore_auto_tag": "Ignore Auto Tag", "image": "Image", @@ -1065,6 +1066,7 @@ "videos": "Videos", "view_all": "View All", "weight": "Weight", + "weight_kg": "Weight (kg)", "years_old": "years old", "zip_file_count": "Zip File Count" } diff --git a/ui/v2.5/src/models/list-filter/criteria/factory.ts b/ui/v2.5/src/models/list-filter/criteria/factory.ts index a2e18c08f..e37384683 100644 --- a/ui/v2.5/src/models/list-filter/criteria/factory.ts +++ b/ui/v2.5/src/models/list-filter/criteria/factory.ts @@ -147,10 +147,14 @@ export function makeCriteria(type: CriterionType = "none") { return new DuplicatedCriterion(); case "country": return new CountryCriterion(); + case "height": + case "height_cm": + return new NumberCriterion( + new NumberCriterionOption("height", "height_cm", type) + ); case "ethnicity": case "hair_color": case "eye_color": - case "height": case "measurements": case "fake_tits": case "career_length": diff --git a/ui/v2.5/src/models/list-filter/performers.ts b/ui/v2.5/src/models/list-filter/performers.ts index 2792997a0..c2836544a 100644 --- a/ui/v2.5/src/models/list-filter/performers.ts +++ b/ui/v2.5/src/models/list-filter/performers.ts @@ -3,6 +3,7 @@ import { createMandatoryNumberCriterionOption, createStringCriterionOption, createBooleanCriterionOption, + NumberCriterionOption, } from "./criteria/criterion"; import { FavoriteCriterionOption } from "./criteria/favorite"; import { GenderCriterionOption } from "./criteria/gender"; @@ -58,7 +59,6 @@ const stringCriteria: CriterionType[] = [ "country", "hair_color", "eye_color", - "height", "measurements", "fake_tits", "career_length", @@ -81,6 +81,7 @@ const criterionOptions = [ createMandatoryNumberCriterionOption("image_count"), createMandatoryNumberCriterionOption("gallery_count"), createBooleanCriterionOption("ignore_auto_tag"), + new NumberCriterionOption("height", "height_cm", "height_cm"), ...numberCriteria.map((c) => createNumberCriterionOption(c)), ...stringCriteria.map((c) => createStringCriterionOption(c)), ]; diff --git a/ui/v2.5/src/models/list-filter/types.ts b/ui/v2.5/src/models/list-filter/types.ts index 8d5fb9d1e..89dec60b1 100644 --- a/ui/v2.5/src/models/list-filter/types.ts +++ b/ui/v2.5/src/models/list-filter/types.ts @@ -88,6 +88,7 @@ export type CriterionType = | "hair_color" | "eye_color" | "height" + | "height_cm" | "weight" | "measurements" | "fake_tits"