diff --git a/graphql/documents/data/performer.graphql b/graphql/documents/data/performer.graphql index 2048da256..09b9e1e68 100644 --- a/graphql/documents/data/performer.graphql +++ b/graphql/documents/data/performer.graphql @@ -31,4 +31,8 @@ fragment PerformerData on Performer { stash_id endpoint } + details + death_date + hair_color + weight } diff --git a/graphql/documents/data/scrapers.graphql b/graphql/documents/data/scrapers.graphql index b4397cdf3..cda034f73 100644 --- a/graphql/documents/data/scrapers.graphql +++ b/graphql/documents/data/scrapers.graphql @@ -19,6 +19,10 @@ fragment ScrapedPerformerData on ScrapedPerformer { ...ScrapedSceneTagData } image + details + death_date + hair_color + weight } fragment ScrapedScenePerformerData on ScrapedScenePerformer { @@ -44,6 +48,10 @@ fragment ScrapedScenePerformerData on ScrapedScenePerformer { } remote_site_id images + details + death_date + hair_color + weight } fragment ScrapedMovieStudioData on ScrapedMovieStudio { diff --git a/graphql/documents/data/studio-slim.graphql b/graphql/documents/data/studio-slim.graphql index a247b4e34..563375e78 100644 --- a/graphql/documents/data/studio-slim.graphql +++ b/graphql/documents/data/studio-slim.graphql @@ -9,4 +9,5 @@ fragment SlimStudioData on Studio { parent_studio { id } + details } diff --git a/graphql/documents/data/studio.graphql b/graphql/documents/data/studio.graphql index 2c6c8d0a3..ae8d1d0d8 100644 --- a/graphql/documents/data/studio.graphql +++ b/graphql/documents/data/studio.graphql @@ -31,4 +31,5 @@ fragment StudioData on Studio { stash_id endpoint } + details } diff --git a/graphql/documents/mutations/performer.graphql b/graphql/documents/mutations/performer.graphql index e4ccf442e..0e2ad9fa3 100644 --- a/graphql/documents/mutations/performer.graphql +++ b/graphql/documents/mutations/performer.graphql @@ -1,47 +1,7 @@ mutation PerformerCreate( - $name: String!, - $url: String, - $gender: GenderEnum, - $birthdate: String, - $ethnicity: String, - $country: String, - $eye_color: String, - $height: String, - $measurements: String, - $fake_tits: String, - $career_length: String, - $tattoos: String, - $piercings: String, - $aliases: String, - $twitter: String, - $instagram: String, - $favorite: Boolean, - $tag_ids: [ID!], - $stash_ids: [StashIDInput!], - $image: String) { + $input: PerformerCreateInput!) { - performerCreate(input: { - name: $name, - url: $url, - gender: $gender, - birthdate: $birthdate, - ethnicity: $ethnicity, - country: $country, - eye_color: $eye_color, - height: $height, - measurements: $measurements, - fake_tits: $fake_tits, - career_length: $career_length, - tattoos: $tattoos, - piercings: $piercings, - aliases: $aliases, - twitter: $twitter, - instagram: $instagram, - favorite: $favorite, - tag_ids: $tag_ids, - stash_ids: $stash_ids, - image: $image - }) { + performerCreate(input: $input) { ...PerformerData } } diff --git a/graphql/documents/mutations/studio.graphql b/graphql/documents/mutations/studio.graphql index d2d11d222..e4cffae84 100644 --- a/graphql/documents/mutations/studio.graphql +++ b/graphql/documents/mutations/studio.graphql @@ -1,11 +1,7 @@ mutation StudioCreate( - $name: String!, - $url: String, - $image: String, - $stash_ids: [StashIDInput!], - $parent_id: ID) { + $input: StudioCreateInput!) { - studioCreate(input: { name: $name, url: $url, image: $image, stash_ids: $stash_ids, parent_id: $parent_id }) { + studioCreate(input: $input) { ...StudioData } } diff --git a/graphql/documents/queries/scrapers/freeones.graphql b/graphql/documents/queries/scrapers/freeones.graphql index 27f6eb926..9f366786d 100644 --- a/graphql/documents/queries/scrapers/freeones.graphql +++ b/graphql/documents/queries/scrapers/freeones.graphql @@ -15,6 +15,10 @@ query ScrapeFreeones($performer_name: String!) { tattoos piercings aliases + details + death_date + hair_color + weight } } diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index db4c6c426..6152174eb 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -73,6 +73,12 @@ input PerformerFilterType { stash_id: String """Filter by url""" url: StringCriterionInput + """Filter by hair color""" + hair_color: StringCriterionInput + """Filter by weight""" + weight: StringCriterionInput + """Filter by death year""" + death_year: IntCriterionInput } input SceneMarkerFilterType { diff --git a/graphql/schema/types/performer.graphql b/graphql/schema/types/performer.graphql index 1c6d642e0..e85bb5d9c 100644 --- a/graphql/schema/types/performer.graphql +++ b/graphql/schema/types/performer.graphql @@ -35,6 +35,10 @@ type Performer { gallery_count: Int # Resolver scenes: [Scene!]! stash_ids: [StashID!]! + details: String + death_date: String + hair_color: String + weight: Int } input PerformerCreateInput { @@ -59,6 +63,10 @@ input PerformerCreateInput { """This should be a URL or a base64 encoded data URL""" image: String stash_ids: [StashIDInput!] + details: String + death_date: String + hair_color: String + weight: Int } input PerformerUpdateInput { @@ -84,6 +92,10 @@ input PerformerUpdateInput { """This should be a URL or a base64 encoded data URL""" image: String stash_ids: [StashIDInput!] + details: String + death_date: String + hair_color: String + weight: Int } input BulkPerformerUpdateInput { @@ -106,6 +118,10 @@ input BulkPerformerUpdateInput { instagram: String favorite: Boolean tag_ids: BulkUpdateIds + details: String + death_date: String + hair_color: String + weight: Int } input PerformerDestroyInput { diff --git a/graphql/schema/types/scraped-performer.graphql b/graphql/schema/types/scraped-performer.graphql index 9f4583600..db6c216a3 100644 --- a/graphql/schema/types/scraped-performer.graphql +++ b/graphql/schema/types/scraped-performer.graphql @@ -21,6 +21,10 @@ type ScrapedPerformer { """This should be a base64 encoded data URL""" image: String + details: String + death_date: String + hair_color: String + weight: String } input ScrapedPerformerInput { @@ -43,4 +47,8 @@ input ScrapedPerformerInput { # not including tags for the input # not including image for the input + details: String + death_date: String + hair_color: String + weight: String } \ No newline at end of file diff --git a/graphql/schema/types/scraper.graphql b/graphql/schema/types/scraper.graphql index 929c13f2b..0a0cec8c5 100644 --- a/graphql/schema/types/scraper.graphql +++ b/graphql/schema/types/scraper.graphql @@ -49,6 +49,10 @@ type ScrapedScenePerformer { remote_site_id: String images: [String!] + details: String + death_date: String + hair_color: String + weight: String } type ScrapedSceneMovie { diff --git a/graphql/schema/types/studio.graphql b/graphql/schema/types/studio.graphql index 1adb0aa63..25145b823 100644 --- a/graphql/schema/types/studio.graphql +++ b/graphql/schema/types/studio.graphql @@ -11,6 +11,7 @@ type Studio { image_count: Int # Resolver gallery_count: Int # Resolver stash_ids: [StashID!]! + details: String } input StudioCreateInput { @@ -20,6 +21,7 @@ input StudioCreateInput { """This should be a URL or a base64 encoded data URL""" image: String stash_ids: [StashIDInput!] + details: String } input StudioUpdateInput { @@ -30,6 +32,7 @@ input StudioUpdateInput { """This should be a URL or a base64 encoded data URL""" image: String stash_ids: [StashIDInput!] + details: String } input StudioDestroyInput { diff --git a/graphql/stash-box/query.graphql b/graphql/stash-box/query.graphql index 4424ce8db..0e74c92c3 100644 --- a/graphql/stash-box/query.graphql +++ b/graphql/stash-box/query.graphql @@ -75,6 +75,11 @@ fragment PerformerFragment on Performer { piercings { ...BodyModificationFragment } + details + death_date { + ...FuzzyDateFragment + } + weight } fragment PerformerAppearanceFragment on PerformerAppearance { diff --git a/pkg/api/resolver_model_performer.go b/pkg/api/resolver_model_performer.go index ab3d2363f..c74ffe95d 100644 --- a/pkg/api/resolver_model_performer.go +++ b/pkg/api/resolver_model_performer.go @@ -208,3 +208,32 @@ func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) return ret, nil } + +func (r *performerResolver) Details(ctx context.Context, obj *models.Performer) (*string, error) { + if obj.Details.Valid { + return &obj.Details.String, nil + } + return nil, nil +} + +func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer) (*string, error) { + if obj.DeathDate.Valid { + return &obj.DeathDate.String, nil + } + return nil, nil +} + +func (r *performerResolver) HairColor(ctx context.Context, obj *models.Performer) (*string, error) { + if obj.HairColor.Valid { + return &obj.HairColor.String, nil + } + return nil, nil +} + +func (r *performerResolver) Weight(ctx context.Context, obj *models.Performer) (*int, error) { + if obj.Weight.Valid { + weight := int(obj.Weight.Int64) + return &weight, nil + } + return nil, nil +} diff --git a/pkg/api/resolver_model_studio.go b/pkg/api/resolver_model_studio.go index 553c8cc5c..da5a1b8b7 100644 --- a/pkg/api/resolver_model_studio.go +++ b/pkg/api/resolver_model_studio.go @@ -116,3 +116,10 @@ func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) (ret return ret, nil } + +func (r *studioResolver) Details(ctx context.Context, obj *models.Studio) (*string, error) { + if obj.Details.Valid { + return &obj.Details.String, nil + } + return nil, nil +} diff --git a/pkg/api/resolver_mutation_performer.go b/pkg/api/resolver_mutation_performer.go index 69eb5832c..9b7feee5c 100644 --- a/pkg/api/resolver_mutation_performer.go +++ b/pkg/api/resolver_mutation_performer.go @@ -3,10 +3,12 @@ package api import ( "context" "database/sql" + "fmt" "strconv" "time" "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/performer" "github.com/stashapp/stash/pkg/utils" ) @@ -83,6 +85,25 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per } else { newPerformer.Favorite = sql.NullBool{Bool: false, Valid: true} } + if input.Details != nil { + newPerformer.Details = sql.NullString{String: *input.Details, Valid: true} + } + if input.DeathDate != nil { + newPerformer.DeathDate = models.SQLiteDate{String: *input.DeathDate, Valid: true} + } + if input.HairColor != nil { + newPerformer.HairColor = sql.NullString{String: *input.HairColor, Valid: true} + } + if input.Weight != nil { + weight := int64(*input.Weight) + newPerformer.Weight = sql.NullInt64{Int64: weight, Valid: true} + } + + if err := performer.ValidateDeathDate(nil, input.Birthdate, input.DeathDate); err != nil { + if err != nil { + return nil, err + } + } // Start the transaction and save the performer var performer *models.Performer @@ -177,33 +198,52 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter") updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram") updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite") + updatedPerformer.Details = translator.nullString(input.Details, "details") + updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date") + updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color") + updatedPerformer.Weight = translator.nullInt64(input.Weight, "weight") - // Start the transaction and save the performer - var performer *models.Performer + // Start the transaction and save the p + var p *models.Performer if err := r.withTxn(ctx, func(repo models.Repository) error { qb := repo.Performer() - var err error - performer, err = qb.Update(updatedPerformer) + // need to get existing performer + existing, err := qb.Find(updatedPerformer.ID) + if err != nil { + return err + } + + if existing == nil { + return fmt.Errorf("performer with id %d not found", updatedPerformer.ID) + } + + if err := performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate); err != nil { + if err != nil { + return err + } + } + + p, err = qb.Update(updatedPerformer) if err != nil { return err } // Save the tags if translator.hasField("tag_ids") { - if err := r.updatePerformerTags(qb, performer.ID, input.TagIds); err != nil { + if err := r.updatePerformerTags(qb, p.ID, input.TagIds); err != nil { return err } } // update image table if len(imageData) > 0 { - if err := qb.UpdateImage(performer.ID, imageData); err != nil { + if err := qb.UpdateImage(p.ID, imageData); err != nil { return err } } else if imageIncluded { // must be unsetting - if err := qb.DestroyImage(performer.ID); err != nil { + if err := qb.DestroyImage(p.ID); err != nil { return err } } @@ -221,7 +261,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per return nil, err } - return performer, nil + return p, nil } func (r *mutationResolver) updatePerformerTags(qb models.PerformerReaderWriter, performerID int, tagsIDs []string) error { @@ -264,6 +304,10 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter") updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram") updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite") + updatedPerformer.Details = translator.nullString(input.Details, "details") + updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date") + updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color") + updatedPerformer.Weight = translator.nullInt64(input.Weight, "weight") if translator.hasField("gender") { if input.Gender != nil { @@ -282,6 +326,20 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models for _, performerID := range performerIDs { updatedPerformer.ID = performerID + // need to get existing performer + existing, err := qb.Find(performerID) + if err != nil { + return err + } + + if existing == nil { + return fmt.Errorf("performer with id %d not found", performerID) + } + + if err := performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate); err != nil { + return err + } + performer, err := qb.Update(updatedPerformer) if err != nil { return err diff --git a/pkg/api/resolver_mutation_studio.go b/pkg/api/resolver_mutation_studio.go index 82be5d1e9..8ec804765 100644 --- a/pkg/api/resolver_mutation_studio.go +++ b/pkg/api/resolver_mutation_studio.go @@ -42,6 +42,10 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio newStudio.ParentID = sql.NullInt64{Int64: parentID, Valid: true} } + if input.Details != nil { + newStudio.Details = sql.NullString{String: *input.Details, Valid: true} + } + // Start the transaction and save the studio var studio *models.Studio if err := r.withTxn(ctx, func(repo models.Repository) error { @@ -109,6 +113,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio } updatedStudio.URL = translator.nullString(input.URL, "url") + updatedStudio.Details = translator.nullString(input.Details, "details") updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id") // Start the transaction and save the studio diff --git a/pkg/database/database.go b/pkg/database/database.go index 95bcb9081..d210e1215 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -23,7 +23,7 @@ import ( var DB *sqlx.DB var WriteMu *sync.Mutex var dbPath string -var appSchemaVersion uint = 20 +var appSchemaVersion uint = 21 var databaseSchemaVersion uint var ( diff --git a/pkg/database/migrations/21_performers_studios_details.up.sql b/pkg/database/migrations/21_performers_studios_details.up.sql new file mode 100644 index 000000000..d41cf4779 --- /dev/null +++ b/pkg/database/migrations/21_performers_studios_details.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE `performers` ADD COLUMN `details` text; +ALTER TABLE `performers` ADD COLUMN `death_date` date; +ALTER TABLE `performers` ADD COLUMN `hair_color` varchar(255); +ALTER TABLE `performers` ADD COLUMN `weight` integer; +ALTER TABLE `studios` ADD COLUMN `details` text; \ No newline at end of file diff --git a/pkg/manager/jsonschema/performer.go b/pkg/manager/jsonschema/performer.go index a145f9bce..a12a617db 100644 --- a/pkg/manager/jsonschema/performer.go +++ b/pkg/manager/jsonschema/performer.go @@ -30,6 +30,10 @@ type Performer struct { Image string `json:"image,omitempty"` CreatedAt models.JSONTime `json:"created_at,omitempty"` UpdatedAt models.JSONTime `json:"updated_at,omitempty"` + Details string `json:"details,omitempty"` + DeathDate string `json:"death_date,omitempty"` + HairColor string `json:"hair_color,omitempty"` + Weight int `json:"weight,omitempty"` } func LoadPerformerFile(filePath string) (*Performer, error) { diff --git a/pkg/manager/jsonschema/studio.go b/pkg/manager/jsonschema/studio.go index ed1f7dea0..d3e55cb08 100644 --- a/pkg/manager/jsonschema/studio.go +++ b/pkg/manager/jsonschema/studio.go @@ -15,6 +15,7 @@ type Studio struct { Image string `json:"image,omitempty"` CreatedAt models.JSONTime `json:"created_at,omitempty"` UpdatedAt models.JSONTime `json:"updated_at,omitempty"` + Details string `json:"details,omitempty"` } func LoadStudioFile(filePath string) (*Studio, error) { diff --git a/pkg/models/model_performer.go b/pkg/models/model_performer.go index 4d6134b8a..0a0ce3f09 100644 --- a/pkg/models/model_performer.go +++ b/pkg/models/model_performer.go @@ -29,6 +29,10 @@ type Performer struct { Favorite sql.NullBool `db:"favorite" json:"favorite"` CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` + Details sql.NullString `db:"details" json:"details"` + DeathDate SQLiteDate `db:"death_date" json:"death_date"` + HairColor sql.NullString `db:"hair_color" json:"hair_color"` + Weight sql.NullInt64 `db:"weight" json:"weight"` } type PerformerPartial struct { @@ -53,6 +57,10 @@ type PerformerPartial struct { Favorite *sql.NullBool `db:"favorite" json:"favorite"` CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"` UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"` + Details *sql.NullString `db:"details" json:"details"` + DeathDate *SQLiteDate `db:"death_date" json:"death_date"` + HairColor *sql.NullString `db:"hair_color" json:"hair_color"` + Weight *sql.NullInt64 `db:"weight" json:"weight"` } func NewPerformer(name string) *Performer { diff --git a/pkg/models/model_scraped_item.go b/pkg/models/model_scraped_item.go index e9fa33118..0c102bcbe 100644 --- a/pkg/models/model_scraped_item.go +++ b/pkg/models/model_scraped_item.go @@ -42,6 +42,10 @@ type ScrapedPerformer struct { Aliases *string `graphql:"aliases" json:"aliases"` Tags []*ScrapedSceneTag `graphql:"tags" json:"tags"` Image *string `graphql:"image" json:"image"` + Details *string `graphql:"details" json:"details"` + DeathDate *string `graphql:"death_date" json:"death_date"` + HairColor *string `graphql:"hair_color" json:"hair_color"` + Weight *string `graphql:"weight" json:"weight"` } // this type has no Image field @@ -63,6 +67,10 @@ type ScrapedPerformerStash struct { Piercings *string `graphql:"piercings" json:"piercings"` Aliases *string `graphql:"aliases" json:"aliases"` Tags []*ScrapedSceneTag `graphql:"tags" json:"tags"` + Details *string `graphql:"details" json:"details"` + DeathDate *string `graphql:"death_date" json:"death_date"` + HairColor *string `graphql:"hair_color" json:"hair_color"` + Weight *string `graphql:"weight" json:"weight"` } type ScrapedScene struct { @@ -128,6 +136,10 @@ type ScrapedScenePerformer struct { Tags []*ScrapedSceneTag `graphql:"tags" json:"tags"` RemoteSiteID *string `graphql:"remote_site_id" json:"remote_site_id"` Images []string `graphql:"images" json:"images"` + Details *string `graphql:"details" json:"details"` + DeathDate *string `graphql:"death_date" json:"death_date"` + HairColor *string `graphql:"hair_color" json:"hair_color"` + Weight *string `graphql:"weight" json:"weight"` } type ScrapedSceneStudio struct { diff --git a/pkg/models/model_studio.go b/pkg/models/model_studio.go index 4bc687526..6336d9163 100644 --- a/pkg/models/model_studio.go +++ b/pkg/models/model_studio.go @@ -15,6 +15,7 @@ type Studio struct { ParentID sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"` CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` + Details sql.NullString `db:"details" json:"details"` } type StudioPartial struct { @@ -25,6 +26,7 @@ type StudioPartial struct { ParentID *sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"` CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"` UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"` + Details *sql.NullString `db:"details" json:"details"` } var DefaultStudioImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4wgVBQsJl1CMZAAAASJJREFUeNrt3N0JwyAYhlEj3cj9R3Cm5rbkqtAP+qrnGaCYHPwJpLlaa++mmLpbAERAgAgIEAEBIiBABERAgAgIEAEBIiBABERAgAgIEAHZuVflj40x4i94zhk9vqsVvEq6AsQqMP1EjORx20OACAgQRRx7T+zzcFBxcjNDfoB4ntQqTm5Awo7MlqywZxcgYQ+RlqywJ3ozJAQCSBiEJSsQA0gYBpDAgAARECACAkRAgAgIEAERECACAmSjUv6eAOSB8m8YIGGzBUjYbAESBgMkbBkDEjZbgITBAClcxiqQvEoatreYIWEBASIgJ4Gkf11ntXH3nS9uxfGWfJ5J9hAgAgJEQAQEiIAAERAgAgJEQAQEiIAAERAgAgJEQAQEiL7qBuc6RKLHxr0CAAAAAElFTkSuQmCC" diff --git a/pkg/performer/export.go b/pkg/performer/export.go index a038a2560..8433353a7 100644 --- a/pkg/performer/export.go +++ b/pkg/performer/export.go @@ -66,6 +66,18 @@ func ToJSON(reader models.PerformerReader, performer *models.Performer) (*jsonsc if performer.Favorite.Valid { newPerformerJSON.Favorite = performer.Favorite.Bool } + if performer.Details.Valid { + newPerformerJSON.Details = performer.Details.String + } + if performer.DeathDate.Valid { + newPerformerJSON.DeathDate = utils.GetYMDFromDatabaseDate(performer.DeathDate.String) + } + if performer.HairColor.Valid { + newPerformerJSON.HairColor = performer.HairColor.String + } + if performer.Weight.Valid { + newPerformerJSON.Weight = int(performer.Weight.Int64) + } image, err := reader.GetImage(performer.ID) if err != nil { diff --git a/pkg/performer/export_test.go b/pkg/performer/export_test.go index aa880e40c..0f082163e 100644 --- a/pkg/performer/export_test.go +++ b/pkg/performer/export_test.go @@ -36,6 +36,9 @@ const ( piercings = "piercings" tattoos = "tattoos" twitter = "twitter" + details = "details" + hairColor = "hairColor" + weight = 60 ) var imageBytes = []byte("imageBytes") @@ -46,6 +49,10 @@ var birthDate = models.SQLiteDate{ String: "2001-01-01", Valid: true, } +var deathDate = models.SQLiteDate{ + String: "2021-02-02", + Valid: true, +} var createTime time.Time = time.Date(2001, 01, 01, 0, 0, 0, 0, time.Local) var updateTime time.Time = time.Date(2002, 01, 01, 0, 0, 0, 0, time.Local) @@ -79,6 +86,13 @@ func createFullPerformer(id int, name string) *models.Performer { UpdatedAt: models.SQLiteTimestamp{ Timestamp: updateTime, }, + Details: models.NullString(details), + DeathDate: deathDate, + HairColor: models.NullString(hairColor), + Weight: sql.NullInt64{ + Int64: weight, + Valid: true, + }, } } @@ -119,7 +133,11 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer { UpdatedAt: models.JSONTime{ Time: updateTime, }, - Image: image, + Image: image, + Details: details, + DeathDate: deathDate.String, + HairColor: hairColor, + Weight: weight, } } diff --git a/pkg/performer/import.go b/pkg/performer/import.go index 2131b1e57..09e0a56a2 100644 --- a/pkg/performer/import.go +++ b/pkg/performer/import.go @@ -224,6 +224,18 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform if performerJSON.Instagram != "" { newPerformer.Instagram = sql.NullString{String: performerJSON.Instagram, Valid: true} } + if performerJSON.Details != "" { + newPerformer.Details = sql.NullString{String: performerJSON.Details, Valid: true} + } + if performerJSON.DeathDate != "" { + newPerformer.DeathDate = models.SQLiteDate{String: performerJSON.DeathDate, Valid: true} + } + if performerJSON.HairColor != "" { + newPerformer.HairColor = sql.NullString{String: performerJSON.HairColor, Valid: true} + } + if performerJSON.Weight != 0 { + newPerformer.Weight = sql.NullInt64{Int64: int64(performerJSON.Weight), Valid: true} + } return newPerformer } diff --git a/pkg/performer/validate.go b/pkg/performer/validate.go new file mode 100644 index 000000000..374262590 --- /dev/null +++ b/pkg/performer/validate.go @@ -0,0 +1,37 @@ +package performer + +import ( + "errors" + + "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/utils" +) + +func ValidateDeathDate(performer *models.Performer, birthdate *string, deathDate *string) error { + // don't validate existing values + if birthdate == nil && deathDate == nil { + return nil + } + + if performer != nil { + if birthdate == nil && performer.Birthdate.Valid { + birthdate = &performer.Birthdate.String + } + if deathDate == nil && performer.DeathDate.Valid { + deathDate = &performer.DeathDate.String + } + } + + if birthdate == nil || deathDate == nil || *birthdate == "" || *deathDate == "" { + return nil + } + + f, _ := utils.ParseDateStringAsTime(*birthdate) + t, _ := utils.ParseDateStringAsTime(*deathDate) + + if f.After(t) { + return errors.New("the date of death should be higher than the date of birth") + } + + return nil +} diff --git a/pkg/performer/validate_test.go b/pkg/performer/validate_test.go new file mode 100644 index 000000000..33616e184 --- /dev/null +++ b/pkg/performer/validate_test.go @@ -0,0 +1,70 @@ +package performer + +import ( + "testing" + + "github.com/stashapp/stash/pkg/models" + "github.com/stretchr/testify/assert" +) + +func TestValidateDeathDate(t *testing.T) { + assert := assert.New(t) + + date1 := "2001-01-01" + date2 := "2002-01-01" + date3 := "2003-01-01" + date4 := "2004-01-01" + empty := "" + + emptyPerformer := models.Performer{} + invalidPerformer := models.Performer{ + Birthdate: models.SQLiteDate{ + String: date3, + Valid: true, + }, + DeathDate: models.SQLiteDate{ + String: date2, + Valid: true, + }, + } + validPerformer := models.Performer{ + Birthdate: models.SQLiteDate{ + String: date2, + Valid: true, + }, + DeathDate: models.SQLiteDate{ + String: date3, + Valid: true, + }, + } + + // nil values should always return nil + assert.Nil(ValidateDeathDate(nil, nil, &date1)) + assert.Nil(ValidateDeathDate(nil, &date2, nil)) + assert.Nil(ValidateDeathDate(&emptyPerformer, nil, &date1)) + assert.Nil(ValidateDeathDate(&emptyPerformer, &date2, nil)) + + // empty strings should always return nil + assert.Nil(ValidateDeathDate(nil, &empty, &date1)) + assert.Nil(ValidateDeathDate(nil, &date2, &empty)) + assert.Nil(ValidateDeathDate(&emptyPerformer, &empty, &date1)) + assert.Nil(ValidateDeathDate(&emptyPerformer, &date2, &empty)) + assert.Nil(ValidateDeathDate(&validPerformer, &empty, &date1)) + assert.Nil(ValidateDeathDate(&validPerformer, &date2, &empty)) + + // nil inputs should return nil even if performer is invalid + assert.Nil(ValidateDeathDate(&invalidPerformer, nil, nil)) + + // invalid input values should return error + assert.NotNil(ValidateDeathDate(nil, &date2, &date1)) + assert.NotNil(ValidateDeathDate(&validPerformer, &date2, &date1)) + + // valid input values should return nil + assert.Nil(ValidateDeathDate(nil, &date1, &date2)) + + // use performer values if performer set and values available + assert.NotNil(ValidateDeathDate(&validPerformer, nil, &date1)) + assert.NotNil(ValidateDeathDate(&validPerformer, &date4, nil)) + assert.Nil(ValidateDeathDate(&validPerformer, nil, &date4)) + assert.Nil(ValidateDeathDate(&validPerformer, &date1, nil)) +} diff --git a/pkg/scraper/freeones.go b/pkg/scraper/freeones.go index 8b72e9df3..c229e874a 100644 --- a/pkg/scraper/freeones.go +++ b/pkg/scraper/freeones.go @@ -103,7 +103,23 @@ xPathScrapers: selector: //div[contains(@class,'image-container')]//a/img/@src Gender: fixed: "Female" -# Last updated March 24, 2021 + Details: //div[@data-test="biography"] + DeathDate: + selector: //div[contains(text(),'Passed away on')] + postProcess: + - replace: + - regex: Passed away on (.+) at the age of \d+ + with: $1 + - parseDate: January 2, 2006 + HairColor: //span[text()='Hair Color']/following-sibling::span/a + Weight: + selector: //span[text()='Weight']/following-sibling::span/a + postProcess: + - replace: + - regex: \D+[\s\S]+ + with: "" + +# Last updated April 13, 2021 ` func getFreeonesScraper() config { diff --git a/pkg/scraper/json_test.go b/pkg/scraper/json_test.go index 6145cc88b..271d83235 100644 --- a/pkg/scraper/json_test.go +++ b/pkg/scraper/json_test.go @@ -23,6 +23,9 @@ jsonScrapers: Piercings: $extras.piercings Aliases: data.aliases Image: data.image + Details: data.bio + HairColor: $extras.hair_colour + Weight: $extras.weight ` const json = ` @@ -41,7 +44,7 @@ jsonScrapers: "ethnicity": "Caucasian", "nationality": "United States", "hair_colour": "Blonde", - "weight": "126 lbs (or 57 kg)", + "weight": 57, "height": "5'6\" (or 167 cm)", "measurements": "34-26-36", "cupsize": "34C (75C)", @@ -90,4 +93,7 @@ jsonScrapers: verifyField(t, "5'6\" (or 167 cm)", scrapedPerformer.Height, "Height") verifyField(t, "None", scrapedPerformer.Tattoos, "Tattoos") verifyField(t, "Navel", scrapedPerformer.Piercings, "Piercings") + verifyField(t, "Some girls are so damn hot that they can get you bent out of shape, and you will not even be mad at them for doing so. Well, tawny blonde Mia Malkova can bend her body into any shape she pleases, and that’s sure to satisfy all of the horny cocks and wet pussies out there. This girl has acrobatic and contortionist abilities that could even twist a pretzel into a new knot, which can be very helpful in the ... arrow_drop_down Some girls are so damn hot that they can get you bent out of shape, and you will not even be mad at them for doing so. Well, tawny blonde Mia Malkova can bend her body into any shape she pleases, and that’s sure to satisfy all of the horny cocks and wet pussies out there. This girl has acrobatic and contortionist abilities that could even twist a pretzel into a new knot, which can be very helpful in the VR Porn movies – trust us. Ankles behind her neck and feet over her back so she can kiss her toes, turned, twisted and gyrating, she can fuck any which way she wants (and that ass!), will surely make you fall in love with this hot Virtual Reality Porn slut, as she is one of the finest of them all. Talking about perfection, maybe it’s all the acrobatic work that keeps it in such gorgeous shape? Who cares really, because you just want to take a big bite out of it and never let go. But it’s not all about the body. Mia’s also got a great smile, which might not sound kinky, but believe us, it is a smile that will heat up your innards and drop your pants. Is it her golden skin, her innocent pink lips or that heart-shaped face? There is just too much good stuff going on with Mia Malkova, which is maybe why these past few years have heaped awards upon awards on this Southern California native. Mia came to VR Bangers for her first VR Porn video, so you know she’s only going for top-notch scenes with top-game performers, men, and women. Better hit up that yoga studio if you ever dream of being able to bang a flexible and talented chick like lady Malkova. arrow_drop_up", scrapedPerformer.Details, "Details") + verifyField(t, "Blonde", scrapedPerformer.HairColor, "HairColor") + verifyField(t, "57", scrapedPerformer.Weight, "Weight") } diff --git a/pkg/scraper/xpath_test.go b/pkg/scraper/xpath_test.go index efc5968a5..d0883e16f 100644 --- a/pkg/scraper/xpath_test.go +++ b/pkg/scraper/xpath_test.go @@ -100,6 +100,14 @@ const htmlDoc1 = ` 5ft7 + + + Weight: + + + 57 + + Measurements: @@ -141,6 +149,14 @@ const htmlDoc1 = ` ; + + + Details: + + + Some girls are so damn hot that they can get you bent out of shape, and you will not even be mad at them for doing so. Well, tawny blonde Mia Malkova can bend her body into any shape she pleases, and that’s sure to satisfy all of the horny cocks and wet pussies out there. This girl has acrobatic and contortionist abilities that could even twist a pretzel into a new knot, which can be very helpful in the ... arrow_drop_down Some girls are so damn hot that they can get you bent out of shape, and you will not even be mad at them for doing so. Well, tawny blonde Mia Malkova can bend her body into any shape she pleases, and that’s sure to satisfy all of the horny cocks and wet pussies out there. This girl has acrobatic and contortionist abilities that could even twist a pretzel into a new knot, which can be very helpful in the VR Porn movies – trust us. Ankles behind her neck and feet over her back so she can kiss her toes, turned, twisted and gyrating, she can fuck any which way she wants (and that ass!), will surely make you fall in love with this hot Virtual Reality Porn slut, as she is one of the finest of them all. Talking about perfection, maybe it’s all the acrobatic work that keeps it in such gorgeous shape? Who cares really, because you just want to take a big bite out of it and never let go. But it’s not all about the body. Mia’s also got a great smile, which might not sound kinky, but believe us, it is a smile that will heat up your innards and drop your pants. Is it her golden skin, her innocent pink lips or that heart-shaped face? There is just too much good stuff going on with Mia Malkova, which is maybe why these past few years have heaped awards upon awards on this Southern California native. Mia came to VR Bangers for her first VR Porn video, so you know she’s only going for top-notch scenes with top-game performers, men, and women. Better hit up that yoga studio if you ever dream of being able to bang a flexible and talented chick like lady Malkova. + +
Social Network Links:
@@ -194,6 +210,9 @@ func makeXPathConfig() mappedPerformerScraperConfig { config.mappedConfig["FakeTits"] = makeSimpleAttrConfig(makeCommonXPath("Fake boobs:")) config.mappedConfig["Tattoos"] = makeSimpleAttrConfig(makeCommonXPath("Tattoos:")) config.mappedConfig["Piercings"] = makeSimpleAttrConfig(makeCommonXPath("Piercings:") + "/comment()") + config.mappedConfig["Details"] = makeSimpleAttrConfig(makeCommonXPath("Details:")) + config.mappedConfig["HairColor"] = makeSimpleAttrConfig(makeCommonXPath("Hair Color:")) + config.mappedConfig["Weight"] = makeSimpleAttrConfig(makeCommonXPath("Weight:")) // special handling for birthdate birthdateAttrConfig := makeSimpleAttrConfig(makeCommonXPath("Date of Birth:")) @@ -299,6 +318,9 @@ func TestScrapePerformerXPath(t *testing.T) { const piercings = "" const gender = "Female" const height = "170" + const details = "Some girls are so damn hot that they can get you bent out of shape, and you will not even be mad at them for doing so. Well, tawny blonde Mia Malkova can bend her body into any shape she pleases, and that’s sure to satisfy all of the horny cocks and wet pussies out there. This girl has acrobatic and contortionist abilities that could even twist a pretzel into a new knot, which can be very helpful in the ... arrow_drop_down Some girls are so damn hot that they can get you bent out of shape, and you will not even be mad at them for doing so. Well, tawny blonde Mia Malkova can bend her body into any shape she pleases, and that’s sure to satisfy all of the horny cocks and wet pussies out there. This girl has acrobatic and contortionist abilities that could even twist a pretzel into a new knot, which can be very helpful in the VR Porn movies – trust us. Ankles behind her neck and feet over her back so she can kiss her toes, turned, twisted and gyrating, she can fuck any which way she wants (and that ass!), will surely make you fall in love with this hot Virtual Reality Porn slut, as she is one of the finest of them all. Talking about perfection, maybe it’s all the acrobatic work that keeps it in such gorgeous shape? Who cares really, because you just want to take a big bite out of it and never let go. But it’s not all about the body. Mia’s also got a great smile, which might not sound kinky, but believe us, it is a smile that will heat up your innards and drop your pants. Is it her golden skin, her innocent pink lips or that heart-shaped face? There is just too much good stuff going on with Mia Malkova, which is maybe why these past few years have heaped awards upon awards on this Southern California native. Mia came to VR Bangers for her first VR Porn video, so you know she’s only going for top-notch scenes with top-game performers, men, and women. Better hit up that yoga studio if you ever dream of being able to bang a flexible and talented chick like lady Malkova." + const hairColor = "Blonde" + const weight = "57" verifyField(t, performerName, performer.Name, "Name") verifyField(t, gender, performer.Gender, "Gender") @@ -317,6 +339,9 @@ func TestScrapePerformerXPath(t *testing.T) { verifyField(t, tattoos, performer.Tattoos, "Tattoos") verifyField(t, piercings, performer.Piercings, "Piercings") verifyField(t, height, performer.Height, "Height") + verifyField(t, details, performer.Details, "Details") + verifyField(t, hairColor, performer.HairColor, "HairColor") + verifyField(t, weight, performer.Weight, "Weight") } func TestConcatXPath(t *testing.T) { diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index fd744f983..bbfcbee92 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -4,7 +4,6 @@ import ( "database/sql" "fmt" "strconv" - "time" "github.com/stashapp/stash/pkg/models" ) @@ -209,7 +208,13 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy } if birthYear := performerFilter.BirthYear; birthYear != nil { - clauses, thisArgs := getBirthYearFilterClause(birthYear.Modifier, birthYear.Value) + clauses, thisArgs := getYearFilterClause(birthYear.Modifier, birthYear.Value, "birthdate") + query.addWhere(clauses...) + query.addArg(thisArgs...) + } + + if deathYear := performerFilter.DeathYear; deathYear != nil { + clauses, thisArgs := getYearFilterClause(deathYear.Modifier, deathYear.Value, "death_date") query.addWhere(clauses...) query.addArg(thisArgs...) } @@ -254,6 +259,8 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy query.handleStringCriterionInput(performerFilter.CareerLength, tableName+".career_length") query.handleStringCriterionInput(performerFilter.Tattoos, tableName+".tattoos") query.handleStringCriterionInput(performerFilter.Piercings, tableName+".piercings") + query.handleStringCriterionInput(performerFilter.HairColor, tableName+".hair_color") + query.handleStringCriterionInput(performerFilter.Weight, tableName+".weight") query.handleStringCriterionInput(performerFilter.URL, tableName+".url") // TODO - need better handling of aliases @@ -294,7 +301,7 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy return performers, countResult, nil } -func getBirthYearFilterClause(criterionModifier models.CriterionModifier, value int) ([]string, []interface{}) { +func getYearFilterClause(criterionModifier models.CriterionModifier, value int, col string) ([]string, []interface{}) { var clauses []string var args []interface{} @@ -306,22 +313,22 @@ func getBirthYearFilterClause(criterionModifier models.CriterionModifier, value switch modifier { case "EQUALS": // between yyyy-01-01 and yyyy-12-31 - clauses = append(clauses, "performers.birthdate >= ?") - clauses = append(clauses, "performers.birthdate <= ?") + clauses = append(clauses, "performers."+col+" >= ?") + clauses = append(clauses, "performers."+col+" <= ?") args = append(args, startOfYear) args = append(args, endOfYear) case "NOT_EQUALS": // outside of yyyy-01-01 to yyyy-12-31 - clauses = append(clauses, "performers.birthdate < ? OR performers.birthdate > ?") + clauses = append(clauses, "performers."+col+" < ? OR performers."+col+" > ?") args = append(args, startOfYear) args = append(args, endOfYear) case "GREATER_THAN": // > yyyy-12-31 - clauses = append(clauses, "performers.birthdate > ?") + clauses = append(clauses, "performers."+col+" > ?") args = append(args, endOfYear) case "LESS_THAN": // < yyyy-01-01 - clauses = append(clauses, "performers.birthdate < ?") + clauses = append(clauses, "performers."+col+" < ?") args = append(args, startOfYear) } } @@ -332,33 +339,23 @@ func getBirthYearFilterClause(criterionModifier models.CriterionModifier, value func getAgeFilterClause(criterionModifier models.CriterionModifier, value int) ([]string, []interface{}) { var clauses []string var args []interface{} + var clause string - // get the date at which performer would turn the age specified - dt := time.Now() - birthDate := dt.AddDate(-value-1, 0, 0) - yearAfter := birthDate.AddDate(1, 0, 0) + if criterionModifier.IsValid() { + switch criterionModifier { + case models.CriterionModifierEquals: + clause = " == ?" + case models.CriterionModifierNotEquals: + clause = " != ?" + case models.CriterionModifierGreaterThan: + clause = " > ?" + case models.CriterionModifierLessThan: + clause = " < ?" + } - if modifier := criterionModifier.String(); criterionModifier.IsValid() { - switch modifier { - case "EQUALS": - // between birthDate and yearAfter - clauses = append(clauses, "performers.birthdate >= ?") - clauses = append(clauses, "performers.birthdate < ?") - args = append(args, birthDate) - args = append(args, yearAfter) - case "NOT_EQUALS": - // outside of birthDate and yearAfter - clauses = append(clauses, "performers.birthdate < ? OR performers.birthdate >= ?") - args = append(args, birthDate) - args = append(args, yearAfter) - case "GREATER_THAN": - // < birthDate - clauses = append(clauses, "performers.birthdate < ?") - args = append(args, birthDate) - case "LESS_THAN": - // > yearAfter - clauses = append(clauses, "performers.birthdate >= ?") - args = append(args, yearAfter) + if clause != "" { + clauses = append(clauses, "cast(IFNULL(strftime('%Y.%m%d', performers.death_date), strftime('%Y.%m%d', 'now')) - strftime('%Y.%m%d', performers.birthdate) as int)"+clause) + args = append(args, value) } } diff --git a/pkg/sqlite/performer_test.go b/pkg/sqlite/performer_test.go index 8fb1a7df3..d2f32182b 100644 --- a/pkg/sqlite/performer_test.go +++ b/pkg/sqlite/performer_test.go @@ -214,10 +214,16 @@ func verifyPerformerAge(t *testing.T, ageCriterion models.IntCriterionInput) { now := time.Now() for _, performer := range performers { + cd := now + + if performer.DeathDate.Valid { + cd, _ = time.Parse("2006-01-02", performer.DeathDate.String) + } + bd := performer.Birthdate.String d, _ := time.Parse("2006-01-02", bd) - age := now.Year() - d.Year() - if now.YearDay() < d.YearDay() { + age := cd.Year() - d.Year() + if cd.YearDay() < d.YearDay() { age = age - 1 } diff --git a/pkg/sqlite/scene_test.go b/pkg/sqlite/scene_test.go index c61f71eb0..c0c36922f 100644 --- a/pkg/sqlite/scene_test.go +++ b/pkg/sqlite/scene_test.go @@ -558,10 +558,10 @@ func verifyInt(t *testing.T, value int, criterion models.IntCriterionInput) { assert.NotEqual(criterion.Value, value) } if criterion.Modifier == models.CriterionModifierGreaterThan { - assert.True(value > criterion.Value) + assert.Greater(value, criterion.Value) } if criterion.Modifier == models.CriterionModifierLessThan { - assert.True(value < criterion.Value) + assert.Less(value, criterion.Value) } } diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index 7996f9516..f8a630095 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -657,6 +657,19 @@ func getPerformerBirthdate(index int) string { return birthdate.Format("2006-01-02") } +func getPerformerDeathDate(index int) models.SQLiteDate { + if index != 5 { + return models.SQLiteDate{} + } + + deathDate := time.Now() + deathDate = deathDate.AddDate(-index+1, -1, -1) + return models.SQLiteDate{ + String: deathDate.Format("2006-01-02"), + Valid: true, + } +} + func getPerformerCareerLength(index int) *string { if index%5 == 0 { return nil @@ -691,6 +704,8 @@ func createPerformers(pqb models.PerformerReaderWriter, n int, o int) error { String: getPerformerBirthdate(i), Valid: true, }, + DeathDate: getPerformerDeathDate(i), + Details: sql.NullString{String: getPerformerStringValue(i, "Details"), Valid: true}, } careerLength := getPerformerCareerLength(i) diff --git a/pkg/studio/export.go b/pkg/studio/export.go index f7e72d53d..5f1e3008f 100644 --- a/pkg/studio/export.go +++ b/pkg/studio/export.go @@ -23,6 +23,10 @@ func ToJSON(reader models.StudioReader, studio *models.Studio) (*jsonschema.Stud newStudioJSON.URL = studio.URL.String } + if studio.Details.Valid { + newStudioJSON.Details = studio.Details.String + } + if studio.ParentID.Valid { parent, err := reader.Find(int(studio.ParentID.Int64)) if err != nil { diff --git a/pkg/studio/export_test.go b/pkg/studio/export_test.go index 72807df48..1a453ec2d 100644 --- a/pkg/studio/export_test.go +++ b/pkg/studio/export_test.go @@ -24,10 +24,13 @@ const ( errParentStudioID = 12 ) -const studioName = "testStudio" -const url = "url" +const ( + studioName = "testStudio" + url = "url" + details = "details" -const parentStudioName = "parentStudio" + parentStudioName = "parentStudio" +) var parentStudio models.Studio = models.Studio{ Name: models.NullString(parentStudioName), @@ -37,15 +40,15 @@ var imageBytes = []byte("imageBytes") const image = "aW1hZ2VCeXRlcw==" -var createTime time.Time = time.Date(2001, 01, 01, 0, 0, 0, 0, time.UTC) -var updateTime time.Time = time.Date(2002, 01, 01, 0, 0, 0, 0, time.UTC) +var createTime time.Time = time.Date(2001, 01, 01, 0, 0, 0, 0, time.Local) +var updateTime time.Time = time.Date(2002, 01, 01, 0, 0, 0, 0, time.Local) func createFullStudio(id int, parentID int) models.Studio { - return models.Studio{ - ID: id, - Name: models.NullString(studioName), - URL: models.NullString(url), - ParentID: models.NullInt64(int64(parentID)), + ret := models.Studio{ + ID: id, + Name: models.NullString(studioName), + URL: models.NullString(url), + Details: models.NullString(details), CreatedAt: models.SQLiteTimestamp{ Timestamp: createTime, }, @@ -53,6 +56,12 @@ func createFullStudio(id int, parentID int) models.Studio { Timestamp: updateTime, }, } + + if parentID != 0 { + ret.ParentID = models.NullInt64(int64(parentID)) + } + + return ret } func createEmptyStudio(id int) models.Studio { @@ -69,8 +78,9 @@ func createEmptyStudio(id int) models.Studio { func createFullJSONStudio(parentStudio, image string) *jsonschema.Studio { return &jsonschema.Studio{ - Name: studioName, - URL: url, + Name: studioName, + URL: url, + Details: details, CreatedAt: models.JSONTime{ Time: createTime, }, diff --git a/pkg/studio/import.go b/pkg/studio/import.go index 1e8afcc67..6e38290f6 100644 --- a/pkg/studio/import.go +++ b/pkg/studio/import.go @@ -28,6 +28,7 @@ func (i *Importer) PreImport() error { Checksum: checksum, Name: sql.NullString{String: i.Input.Name, Valid: true}, URL: sql.NullString{String: i.Input.URL, Valid: true}, + Details: sql.NullString{String: i.Input.Details, Valid: true}, CreatedAt: models.SQLiteTimestamp{Timestamp: i.Input.CreatedAt.GetTime()}, UpdatedAt: models.SQLiteTimestamp{Timestamp: i.Input.UpdatedAt.GetTime()}, } diff --git a/pkg/studio/import_test.go b/pkg/studio/import_test.go index 339e8b9b6..29a0d8813 100644 --- a/pkg/studio/import_test.go +++ b/pkg/studio/import_test.go @@ -7,6 +7,7 @@ import ( "github.com/stashapp/stash/pkg/manager/jsonschema" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/mocks" + "github.com/stashapp/stash/pkg/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -51,6 +52,17 @@ func TestImporterPreImport(t *testing.T) { err = i.PreImport() assert.Nil(t, err) + + i.Input = *createFullJSONStudio(studioName, image) + i.Input.ParentStudio = "" + + err = i.PreImport() + + assert.Nil(t, err) + expectedStudio := createFullStudio(0, 0) + expectedStudio.ParentID.Valid = false + expectedStudio.Checksum = utils.MD5FromString(studioName) + assert.Equal(t, expectedStudio, i.studio) } func TestImporterPreImportWithParent(t *testing.T) { diff --git a/ui/v2.5/src/components/Changelog/versions/v070.md b/ui/v2.5/src/components/Changelog/versions/v070.md index f0e5ebffa..153372279 100644 --- a/ui/v2.5/src/components/Changelog/versions/v070.md +++ b/ui/v2.5/src/components/Changelog/versions/v070.md @@ -1,5 +1,11 @@ ### ✨ New Features +* Added details, death date, hair color, and weight to Performers. +* Added details to Studios. * Added [perceptual dupe checker](/settings?tab=duplicates). +* Add various `count` filter criteria and sort options. +* Add URL filter criteria for scenes, galleries, movies, performers and studios. +* Add HTTP endpoint for health checking at `/healthz`. +* Add random sorting option for galleries, studios, movies and tags. * Support access to system without logging in via API key. * Added scene queue. @@ -10,12 +16,8 @@ * Add slideshow to image wall view. * Support API key via URL query parameter, and added API key to stream link in Scene File Info. * Revamped setup wizard and migration UI. -* Add various `count` filter criteria and sort options. * Scroll to top when changing page number. -* Add URL filter criteria for scenes, galleries, movies, performers and studios. -* Add HTTP endpoint for health checking at `/healthz`. * Support `today` and `yesterday` for `parseDate` in scrapers. -* Add random sorting option for galleries, studios, movies and tags. * Disable sounds on scene/marker wall previews by default. * Improve Movie UI. * Change performer text query to search by name and alias only. diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx index 9a71b33ad..147545bae 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx @@ -273,8 +273,10 @@ export const GalleryScrapeDialog: React.FC = ( try { const result = await createStudio({ variables: { - name: toCreate.name, - url: toCreate.url, + input: { + name: toCreate.name, + url: toCreate.url, + }, }, }); @@ -299,7 +301,7 @@ export const GalleryScrapeDialog: React.FC = ( try { performerInput = Object.assign(performerInput, toCreate); const result = await createPerformer({ - variables: performerInput, + variables: { input: performerInput }, }); // add the new performer to the new performers value diff --git a/ui/v2.5/src/components/Performers/PerformerCard.tsx b/ui/v2.5/src/components/Performers/PerformerCard.tsx index 4abf9bcf4..298f5c0d6 100644 --- a/ui/v2.5/src/components/Performers/PerformerCard.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCard.tsx @@ -29,7 +29,10 @@ export const PerformerCard: React.FC = ({ selected, onSelectedChanged, }) => { - const age = TextUtils.age(performer.birthdate, ageFromDate); + const age = TextUtils.age( + performer.birthdate, + ageFromDate ?? performer.death_date + ); const ageString = `${age} years old${ageFromDate ? " in this scene." : "."}`; function maybeRenderFavoriteBanner() { diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index b4e2699c0..5cf92383f 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -160,7 +160,9 @@ export const Performer: React.FC = () => { // provided by the server return (
- {TextUtils.age(performer.birthdate)} + + {TextUtils.age(performer.birthdate, performer.death_date)} + years old
); diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx index be15b3db6..79980287c 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -81,6 +81,17 @@ export const PerformerDetailsPanel: React.FC = ({ }); }; + const formatWeight = (weight?: number | null) => { + if (!weight) { + return ""; + } + return intl.formatNumber(weight, { + style: "unit", + unit: "kilogram", + unitDisplay: "narrow", + }); + }; + return ( <> = ({ name="Birthdate" value={TextUtils.formatDate(intl, performer.birthdate ?? undefined)} /> + + + + = ({ tag_ids: yup.array(yup.string().required()).optional(), stash_ids: yup.mixed().optional(), image: yup.string().optional().nullable(), + details: yup.string().optional(), + death_date: yup.string().optional(), + hair_color: yup.string().optional(), + weight: yup.number().optional(), }); const initialValues = { @@ -131,6 +135,10 @@ export const PerformerEditPanel: React.FC = ({ tag_ids: (performer.tags ?? []).map((t) => t.id), stash_ids: performer.stash_ids ?? undefined, image: undefined, + details: performer.details ?? "", + death_date: performer.death_date ?? "", + hair_color: performer.hair_color ?? "", + weight: performer.weight ?? "", }; type InputValues = typeof initialValues; @@ -306,6 +314,18 @@ export const PerformerEditPanel: React.FC = ({ const imageStr = (state as GQL.ScrapedPerformerDataFragment).image; formik.setFieldValue("image", imageStr ?? undefined); } + if (state.details) { + formik.setFieldValue("details", state.details); + } + if (state.death_date) { + formik.setFieldValue("death_date", state.death_date); + } + if (state.hair_color) { + formik.setFieldValue("hair_color", state.hair_color); + } + if (state.weight) { + formik.setFieldValue("weight", state.weight); + } } function onImageLoad(imageData: string) { @@ -334,7 +354,7 @@ export const PerformerEditPanel: React.FC = ({ history.push(`/performers/${performer.id}`); } else { const result = await createPerformer({ - variables: performerInput as GQL.PerformerCreateInput, + variables: { input: performerInput as GQL.PerformerCreateInput }, }); if (result.data?.performerCreate) { history.push(`/performers/${result.data.performerCreate.id}`); @@ -399,6 +419,7 @@ export const PerformerEditPanel: React.FC = ({ > = { ...values, gender: stringToGender(values.gender), + weight: Number(values.weight), }; if (!isNew) { @@ -550,6 +571,7 @@ export const PerformerEditPanel: React.FC = ({ ...formik.values, gender: stringToGender(formik.values.gender), image: formik.values.image ?? performer.image_path, + weight: Number(formik.values.weight), }; return ( @@ -806,10 +828,13 @@ export const PerformerEditPanel: React.FC = ({ {renderTextField("birthdate", "Birthdate", "YYYY-MM-DD")} + {renderTextField("death_date", "Death Date", "YYYY-MM-DD")} {renderTextField("country", "Country")} {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")} @@ -861,7 +886,19 @@ export const PerformerEditPanel: React.FC = ({ {renderTextField("twitter", "Twitter")} {renderTextField("instagram", "Instagram")} - + + + Details + + + + + {renderTagsField()} {renderStashIDs()} diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx index 4e6305cbb..204a52811 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx @@ -153,18 +153,36 @@ export const PerformerScrapeDialog: React.FC = ( const [birthdate, setBirthdate] = useState>( new ScrapeResult(props.performer.birthdate, props.scraped.birthdate) ); + const [deathDate, setDeathDate] = useState>( + new ScrapeResult( + props.performer.death_date, + props.scraped.death_date + ) + ); const [ethnicity, setEthnicity] = useState>( new ScrapeResult(props.performer.ethnicity, props.scraped.ethnicity) ); const [country, setCountry] = useState>( new ScrapeResult(props.performer.country, props.scraped.country) ); + const [hairColor, setHairColor] = useState>( + new ScrapeResult( + props.performer.hair_color, + props.scraped.hair_color + ) + ); const [eyeColor, setEyeColor] = useState>( new ScrapeResult(props.performer.eye_color, props.scraped.eye_color) ); const [height, setHeight] = useState>( new ScrapeResult(props.performer.height, props.scraped.height) ); + const [weight, setWeight] = useState>( + new ScrapeResult( + props.performer.weight?.toString(), + props.scraped.weight + ) + ); const [measurements, setMeasurements] = useState>( new ScrapeResult( props.performer.measurements, @@ -201,6 +219,9 @@ export const PerformerScrapeDialog: React.FC = ( translateScrapedGender(props.scraped.gender) ) ); + const [details, setDetails] = useState>( + new ScrapeResult(props.performer.details, props.scraped.details) + ); const [createTag] = useTagCreate({ name: "" }); const Toast = useToast(); @@ -281,6 +302,10 @@ export const PerformerScrapeDialog: React.FC = ( gender, image, tags, + details, + deathDate, + hairColor, + weight, ]; // don't show the dialog if nothing was scraped if (allFields.every((r) => !r.scraped)) { @@ -348,6 +373,10 @@ export const PerformerScrapeDialog: React.FC = ( }; }), image: image.getNewValue(), + details: details.getNewValue(), + death_date: deathDate.getNewValue(), + hair_color: hairColor.getNewValue(), + weight: weight.getNewValue(), }; } @@ -370,6 +399,11 @@ export const PerformerScrapeDialog: React.FC = ( result={birthdate} onChange={(value) => setBirthdate(value)} /> + setDeathDate(value)} + /> = ( result={country} onChange={(value) => setCountry(value)} /> + setHairColor(value)} + /> setEyeColor(value)} /> + setWeight(value)} + /> = ( result={instagram} onChange={(value) => setInstagram(value)} /> + setDetails(value)} + /> {renderScrapedTagsRow( tags, (value) => setTags(value), diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx index 77838220c..70b5db8d5 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx @@ -336,8 +336,10 @@ export const SceneScrapeDialog: React.FC = ( try { const result = await createStudio({ variables: { - name: toCreate.name, - url: toCreate.url, + input: { + name: toCreate.name, + url: toCreate.url, + }, }, }); @@ -362,7 +364,7 @@ export const SceneScrapeDialog: React.FC = ( try { performerInput = Object.assign(performerInput, toCreate); const result = await createPerformer({ - variables: performerInput, + variables: { input: performerInput }, }); // add the new performer to the new performers value diff --git a/ui/v2.5/src/components/Shared/Select.tsx b/ui/v2.5/src/components/Shared/Select.tsx index 8ed94b245..e89840bca 100644 --- a/ui/v2.5/src/components/Shared/Select.tsx +++ b/ui/v2.5/src/components/Shared/Select.tsx @@ -381,7 +381,7 @@ export const PerformerSelect: React.FC = (props) => { const onCreate = async (name: string) => { const result = await createPerformer({ - variables: { name }, + variables: { input: { name } }, }); return { item: result.data!.performerCreate!, @@ -415,7 +415,11 @@ export const StudioSelect: React.FC< ); const onCreate = async (name: string) => { - const result = await createStudio({ variables: { name } }); + const result = await createStudio({ + variables: { + input: { name }, + }, + }); return { item: result.data!.studioCreate!, message: "Created studio" }; }; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx index 67dc73454..0cd7c3813 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx @@ -45,6 +45,7 @@ export const Studio: React.FC = () => { const [name, setName] = useState(); const [url, setUrl] = useState(); const [parentStudioId, setParentStudioId] = useState(); + const [details, setDetails] = useState(); // Studio state const [studio, setStudio] = useState>({}); @@ -63,6 +64,7 @@ export const Studio: React.FC = () => { setName(state.name); setUrl(state.url ?? undefined); setParentStudioId(state?.parent_studio?.id ?? undefined); + setDetails(state.details ?? undefined); } function updateStudioData(studioData: Partial) { @@ -117,6 +119,7 @@ export const Studio: React.FC = () => { name, url, image, + details, parent_id: parentStudioId ?? null, }; @@ -301,6 +304,12 @@ export const Studio: React.FC = () => { isEditing: !!isEditing, onChange: setUrl, })} + {TableUtils.renderTextArea({ + title: "Details", + value: details, + isEditing: !!isEditing, + onChange: setDetails, + })} Parent Studio {renderStudio()} diff --git a/ui/v2.5/src/components/Tagger/PerformerModal.tsx b/ui/v2.5/src/components/Tagger/PerformerModal.tsx index c5deb3eee..41233e2ad 100755 --- a/ui/v2.5/src/components/Tagger/PerformerModal.tsx +++ b/ui/v2.5/src/components/Tagger/PerformerModal.tsx @@ -85,6 +85,13 @@ const PerformerModal: React.FC = ({ text={performer.birthdate ?? "Unknown"} /> +
+ Death Date: + +
Ethnicity: = ({ Country:
+
+ Hair Color: + +
Eye Color: = ({ Height:
+
+ Weight: + +
Measurements: diff --git a/ui/v2.5/src/components/Tagger/StashSearchResult.tsx b/ui/v2.5/src/components/Tagger/StashSearchResult.tsx index 9a678b094..bb121e612 100755 --- a/ui/v2.5/src/components/Tagger/StashSearchResult.tsx +++ b/ui/v2.5/src/components/Tagger/StashSearchResult.tsx @@ -222,6 +222,10 @@ const StashSearchResult: React.FC = ({ stash_id: stashID, }, ], + details: performer.data.details, + death_date: performer.data.death_date, + hair_color: performer.data.hair_color, + weight: Number(performer.data.weight), }; const res = await createPerformer(performerInput, stashID); diff --git a/ui/v2.5/src/components/Tagger/queries.ts b/ui/v2.5/src/components/Tagger/queries.ts index 66a91e106..d2ef882e4 100644 --- a/ui/v2.5/src/components/Tagger/queries.ts +++ b/ui/v2.5/src/components/Tagger/queries.ts @@ -55,7 +55,7 @@ export const useCreatePerformer = () => { const handleCreate = (performer: GQL.PerformerCreateInput, stashID: string) => createPerformer({ - variables: performer, + variables: { input: performer }, update: (store, newPerformer) => { if (!newPerformer?.data?.performerCreate) return; @@ -159,7 +159,7 @@ export const useCreateStudio = () => { const handleCreate = (studio: GQL.StudioCreateInput, stashID: string) => createStudio({ - variables: studio, + variables: { input: studio }, update: (store, result) => { if (!result?.data?.studioCreate) return; diff --git a/ui/v2.5/src/components/Tagger/utils.ts b/ui/v2.5/src/components/Tagger/utils.ts index 39b34bd94..b5666bcf0 100644 --- a/ui/v2.5/src/components/Tagger/utils.ts +++ b/ui/v2.5/src/components/Tagger/utils.ts @@ -56,6 +56,10 @@ export interface IStashBoxPerformer { piercings?: string; aliases?: string; images: string[]; + details?: string; + death_date?: string; + hair_color?: string; + weight?: string; } export interface IStashBoxTag { @@ -126,6 +130,9 @@ const selectPerformers = ( piercings: p.piercings ? toTitleCase(p.piercings) : undefined, aliases: p.aliases ?? undefined, images: p.images ?? [], + details: p.details ?? undefined, + death_date: p.death_date ?? undefined, + hair_color: p.hair_color ?? undefined, })); export const selectScenes = ( diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index 1ac8e6d80..2016da8e6 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -584,7 +584,7 @@ export const studioMutationImpactedQueries = [ export const useStudioCreate = (input: GQL.StudioCreateInput) => GQL.useStudioCreateMutation({ - variables: input, + variables: { input }, refetchQueries: getQueryNames([GQL.AllStudiosForFilterDocument]), update: deleteCache([ GQL.FindStudiosDocument, diff --git a/ui/v2.5/src/docs/en/JSONSpec.md b/ui/v2.5/src/docs/en/JSONSpec.md index f8d036d0b..e83141891 100644 --- a/ui/v2.5/src/docs/en/JSONSpec.md +++ b/ui/v2.5/src/docs/en/JSONSpec.md @@ -52,10 +52,13 @@ url twitter instagram birthdate +death_date ethnicity country +hair_color eye_color height +weight measurements fake_tits career_length @@ -64,6 +67,7 @@ piercings image (base64 encoding of the image file) created_at updated_at +details ``` ## Studio @@ -72,7 +76,8 @@ name url image (base64 encoding of the image file) created_at -updated_at +updated_at +details ``` ## Scene @@ -229,6 +234,10 @@ For those preferring the json-format, defined [here](https://json-schema.org/), "description": "Birthdate of the performer. Format is YYYY-MM-DD", "type": "string" }, + "death_date": { + "description": "Death date of the performer. Format is YYYY-MM-DD", + "type": "string" + }, "ethnicity": { "description": "Ethnicity of the Performer. Possible values are black, white, asian or hispanic", "type": "string" @@ -237,6 +246,10 @@ For those preferring the json-format, defined [here](https://json-schema.org/), "description": "Country of the performer", "type": "string" }, + "hair_color": { + "description": "Hair color of the performer", + "type": "string" + }, "eye_color": { "description": "Eye color of the performer", "type": "string" @@ -245,6 +258,10 @@ For those preferring the json-format, defined [here](https://json-schema.org/), "description": "Height of the performer in centimeters", "type": "string" }, + "weight": { + "description": "Weight of the performer in kilograms", + "type": "string" + }, "measurements": { "description": "Measurements of the performer", "type": "string" @@ -276,6 +293,10 @@ For those preferring the json-format, defined [here](https://json-schema.org/), "updated_at": { "description": "The time this performers data was last changed in the database. Format is YYYY-MM-DDThh:mm:ssTZD", "type": "string" + }, + "details": { + "description": "Description of the performer", + "type": "string" } }, "required": ["name", "ethnicity", "image", "created_at", "updated_at"] @@ -312,6 +333,10 @@ For those preferring the json-format, defined [here](https://json-schema.org/), "updated_at": { "description": "The time this studios data was last changed in the database. Format is YYYY-MM-DDThh:mm:ssTZD", "type": "string" + }, + "details": { + "description": "Description of the studio", + "type": "string" } }, "required": ["name", "image", "created_at", "updated_at"] diff --git a/ui/v2.5/src/docs/en/Scraping.md b/ui/v2.5/src/docs/en/Scraping.md index 43a4407e3..40171b62d 100644 --- a/ui/v2.5/src/docs/en/Scraping.md +++ b/ui/v2.5/src/docs/en/Scraping.md @@ -740,10 +740,13 @@ URL Twitter Instagram Birthdate +DeathDate Ethnicity Country +HairColor EyeColor Height +Weight Measurements FakeTits CareerLength @@ -752,6 +755,7 @@ Piercings Aliases Tags (see Tag fields) Image +Details ``` *Note:* - `Gender` must be one of `male`, `female`, `transgender_male`, `transgender_female`, `intersex`, `non_binary` (case insensitive). diff --git a/ui/v2.5/src/models/list-filter/criteria/criterion.ts b/ui/v2.5/src/models/list-filter/criteria/criterion.ts index ee351a3c5..382024976 100644 --- a/ui/v2.5/src/models/list-filter/criteria/criterion.ts +++ b/ui/v2.5/src/models/list-filter/criteria/criterion.ts @@ -34,8 +34,10 @@ export type CriterionType = | "age" | "ethnicity" | "country" + | "hair_color" | "eye_color" | "height" + | "weight" | "measurements" | "fake_tits" | "career_length" @@ -49,6 +51,7 @@ export type CriterionType = | "image_count" | "gallery_count" | "performer_count" + | "death_year" | "url"; type Option = string | number | IOptionType; @@ -103,16 +106,22 @@ export abstract class Criterion { return "Galleries"; case "birth_year": return "Birth Year"; + case "death_year": + return "Death Year"; case "age": return "Age"; case "ethnicity": return "Ethnicity"; case "country": return "Country"; + case "hair_color": + return "Hair Color"; case "eye_color": return "Eye Color"; case "height": return "Height"; + case "weight": + return "Weight"; case "measurements": return "Measurements"; case "fake_tits": diff --git a/ui/v2.5/src/models/list-filter/criteria/is-missing.ts b/ui/v2.5/src/models/list-filter/criteria/is-missing.ts index 6e3cd75d6..d77557e8b 100644 --- a/ui/v2.5/src/models/list-filter/criteria/is-missing.ts +++ b/ui/v2.5/src/models/list-filter/criteria/is-missing.ts @@ -53,8 +53,10 @@ export class PerformerIsMissingCriterion extends IsMissingCriterion { "instagram", "ethnicity", "country", + "hair_color", "eye_color", "height", + "weight", "measurements", "fake_tits", "career_length", @@ -65,6 +67,7 @@ export class PerformerIsMissingCriterion extends IsMissingCriterion { "scenes", "image", "stash_id", + "details", ]; } @@ -104,7 +107,7 @@ export class TagIsMissingCriterionOption implements ICriterionOption { export class StudioIsMissingCriterion extends IsMissingCriterion { public type: CriterionType = "studioIsMissing"; - public options: string[] = ["image", "stash_id"]; + public options: string[] = ["image", "stash_id", "details"]; } export class StudioIsMissingCriterionOption implements ICriterionOption { diff --git a/ui/v2.5/src/models/list-filter/criteria/utils.ts b/ui/v2.5/src/models/list-filter/criteria/utils.ts index 81d37e750..2f1e82030 100644 --- a/ui/v2.5/src/models/list-filter/criteria/utils.ts +++ b/ui/v2.5/src/models/list-filter/criteria/utils.ts @@ -88,6 +88,8 @@ export function makeCriteria(type: CriterionType = "none") { case "galleries": return new GalleriesCriterion(); case "birth_year": + case "death_year": + case "weight": return new NumberCriterion(type, type); case "age": return new MandatoryNumberCriterion(type, type); @@ -95,6 +97,7 @@ export function makeCriteria(type: CriterionType = "none") { return new GenderCriterion(); case "ethnicity": case "country": + case "hair_color": case "eye_color": case "height": case "measurements": diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index fe775f41e..c17d2527f 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -200,12 +200,18 @@ export class ListFilterModel { ]; this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List]; - const numberCriteria: CriterionType[] = ["birth_year", "age"]; + const numberCriteria: CriterionType[] = [ + "birth_year", + "death_year", + "age", + ]; const stringCriteria: CriterionType[] = [ "ethnicity", "country", + "hair_color", "eye_color", "height", + "weight", "measurements", "fake_tits", "career_length", @@ -650,6 +656,14 @@ export class ListFilterModel { }; break; } + case "death_year": { + const dyCrit = criterion as NumberCriterion; + result.death_year = { + value: dyCrit.value, + modifier: dyCrit.modifier, + }; + break; + } case "age": { const ageCrit = criterion as NumberCriterion; result.age = { value: ageCrit.value, modifier: ageCrit.modifier }; @@ -671,6 +685,14 @@ export class ListFilterModel { }; break; } + case "hair_color": { + const hcCrit = criterion as StringCriterion; + result.hair_color = { + value: hcCrit.value, + modifier: hcCrit.modifier, + }; + break; + } case "eye_color": { const ecCrit = criterion as StringCriterion; result.eye_color = { value: ecCrit.value, modifier: ecCrit.modifier }; @@ -681,6 +703,11 @@ export class ListFilterModel { result.height = { value: hCrit.value, modifier: hCrit.modifier }; break; } + case "weight": { + const wCrit = criterion as StringCriterion; + result.weight = { value: wCrit.value, modifier: wCrit.modifier }; + break; + } case "measurements": { const mCrit = criterion as StringCriterion; result.measurements = { diff --git a/ui/v2.5/src/utils/text.ts b/ui/v2.5/src/utils/text.ts index b69c440dc..cf6c512fa 100644 --- a/ui/v2.5/src/utils/text.ts +++ b/ui/v2.5/src/utils/text.ts @@ -83,7 +83,7 @@ const stringToDate = (dateString: string) => { return new Date(year, monthIndex, day, 0, 0, 0, 0); }; -const getAge = (dateString?: string | null, fromDateString?: string) => { +const getAge = (dateString?: string | null, fromDateString?: string | null) => { if (!dateString) return 0; const birthdate = stringToDate(dateString);