mirror of https://github.com/stashapp/stash.git
added details, deathdate, hair color, weight to performers and added details to studios (#1274)
* added details to performers and studios * added deathdate, hair_color and weight to performers * Simplify performer/studio create mutations * Add changelog and recategorised Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
parent
cd6b6b74eb
commit
d673c4ce03
|
@ -31,4 +31,8 @@ fragment PerformerData on Performer {
|
|||
stash_id
|
||||
endpoint
|
||||
}
|
||||
details
|
||||
death_date
|
||||
hair_color
|
||||
weight
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -9,4 +9,5 @@ fragment SlimStudioData on Studio {
|
|||
parent_studio {
|
||||
id
|
||||
}
|
||||
details
|
||||
}
|
||||
|
|
|
@ -31,4 +31,5 @@ fragment StudioData on Studio {
|
|||
stash_id
|
||||
endpoint
|
||||
}
|
||||
details
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,10 @@ query ScrapeFreeones($performer_name: String!) {
|
|||
tattoos
|
||||
piercings
|
||||
aliases
|
||||
details
|
||||
death_date
|
||||
hair_color
|
||||
weight
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -49,6 +49,10 @@ type ScrapedScenePerformer {
|
|||
|
||||
remote_site_id: String
|
||||
images: [String!]
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
weight: String
|
||||
}
|
||||
|
||||
type ScrapedSceneMovie {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -75,6 +75,11 @@ fragment PerformerFragment on Performer {
|
|||
piercings {
|
||||
...BodyModificationFragment
|
||||
}
|
||||
details
|
||||
death_date {
|
||||
...FuzzyDateFragment
|
||||
}
|
||||
weight
|
||||
}
|
||||
|
||||
fragment PerformerAppearanceFragment on PerformerAppearance {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -100,6 +100,14 @@ const htmlDoc1 = `
|
|||
5ft7
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="paramname">
|
||||
<b>Weight:</b>
|
||||
</td>
|
||||
<td class="paramvalue">
|
||||
57
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="paramname">
|
||||
<b>Measurements:</b>
|
||||
|
@ -141,6 +149,14 @@ const htmlDoc1 = `
|
|||
<!-- None -->;
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="paramname">
|
||||
<b>Details:</b>
|
||||
</td>
|
||||
<td class="paramvalue">
|
||||
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.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="paramname">
|
||||
<div><b>Social Network Links:</b></div>
|
||||
|
@ -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 = "<!-- None -->"
|
||||
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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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()},
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -273,8 +273,10 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
|||
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<IGalleryScrapeDialogProps> = (
|
|||
try {
|
||||
performerInput = Object.assign(performerInput, toCreate);
|
||||
const result = await createPerformer({
|
||||
variables: performerInput,
|
||||
variables: { input: performerInput },
|
||||
});
|
||||
|
||||
// add the new performer to the new performers value
|
||||
|
|
|
@ -29,7 +29,10 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||
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() {
|
||||
|
|
|
@ -160,7 +160,9 @@ export const Performer: React.FC = () => {
|
|||
// provided by the server
|
||||
return (
|
||||
<div>
|
||||
<span className="age">{TextUtils.age(performer.birthdate)}</span>
|
||||
<span className="age">
|
||||
{TextUtils.age(performer.birthdate, performer.death_date)}
|
||||
</span>
|
||||
<span className="age-tail"> years old</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -81,6 +81,17 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||
});
|
||||
};
|
||||
|
||||
const formatWeight = (weight?: number | null) => {
|
||||
if (!weight) {
|
||||
return "";
|
||||
}
|
||||
return intl.formatNumber(weight, {
|
||||
style: "unit",
|
||||
unit: "kilogram",
|
||||
unitDisplay: "narrow",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
|
@ -91,15 +102,22 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||
name="Birthdate"
|
||||
value={TextUtils.formatDate(intl, performer.birthdate ?? undefined)}
|
||||
/>
|
||||
<TextField
|
||||
name="Death Date"
|
||||
value={TextUtils.formatDate(intl, performer.death_date ?? undefined)}
|
||||
/>
|
||||
<TextField name="Ethnicity" value={performer.ethnicity} />
|
||||
<TextField name="Hair Color" value={performer.hair_color} />
|
||||
<TextField name="Eye Color" value={performer.eye_color} />
|
||||
<TextField name="Country" value={performer.country} />
|
||||
<TextField name="Height" value={formatHeight(performer.height)} />
|
||||
<TextField name="Weight" value={formatWeight(performer.weight)} />
|
||||
<TextField name="Measurements" value={performer.measurements} />
|
||||
<TextField name="Fake Tits" value={performer.fake_tits} />
|
||||
<TextField name="Career Length" value={performer.career_length} />
|
||||
<TextField name="Tattoos" value={performer.tattoos} />
|
||||
<TextField name="Piercings" value={performer.piercings} />
|
||||
<TextField name="Details" value={performer.details} />
|
||||
<URLField
|
||||
name="URL"
|
||||
value={performer.url}
|
||||
|
|
|
@ -109,6 +109,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
tag_ids: yup.array(yup.string().required()).optional(),
|
||||
stash_ids: yup.mixed<GQL.StashIdInput>().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<IPerformerDetails> = ({
|
|||
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<IPerformerDetails> = ({
|
|||
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<IPerformerDetails> = ({
|
|||
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<IPerformerDetails> = ({
|
|||
> = {
|
||||
...values,
|
||||
gender: stringToGender(values.gender),
|
||||
weight: Number(values.weight),
|
||||
};
|
||||
|
||||
if (!isNew) {
|
||||
|
@ -550,6 +571,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
...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<IPerformerDetails> = ({
|
|||
</Form.Group>
|
||||
|
||||
{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<IPerformerDetails> = ({
|
|||
|
||||
{renderTextField("twitter", "Twitter")}
|
||||
{renderTextField("instagram", "Instagram")}
|
||||
|
||||
<Form.Group controlId="details" as={Row}>
|
||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||
Details
|
||||
</Form.Label>
|
||||
<Col sm={fieldXS} xl={fieldXL}>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="text-input"
|
||||
placeholder="Details"
|
||||
{...formik.getFieldProps("details")}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
{renderTagsField()}
|
||||
{renderStashIDs()}
|
||||
|
||||
|
|
|
@ -153,18 +153,36 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
const [birthdate, setBirthdate] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.birthdate, props.scraped.birthdate)
|
||||
);
|
||||
const [deathDate, setDeathDate] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(
|
||||
props.performer.death_date,
|
||||
props.scraped.death_date
|
||||
)
|
||||
);
|
||||
const [ethnicity, setEthnicity] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.ethnicity, props.scraped.ethnicity)
|
||||
);
|
||||
const [country, setCountry] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.country, props.scraped.country)
|
||||
);
|
||||
const [hairColor, setHairColor] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(
|
||||
props.performer.hair_color,
|
||||
props.scraped.hair_color
|
||||
)
|
||||
);
|
||||
const [eyeColor, setEyeColor] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.eye_color, props.scraped.eye_color)
|
||||
);
|
||||
const [height, setHeight] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.height, props.scraped.height)
|
||||
);
|
||||
const [weight, setWeight] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(
|
||||
props.performer.weight?.toString(),
|
||||
props.scraped.weight
|
||||
)
|
||||
);
|
||||
const [measurements, setMeasurements] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(
|
||||
props.performer.measurements,
|
||||
|
@ -201,6 +219,9 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
translateScrapedGender(props.scraped.gender)
|
||||
)
|
||||
);
|
||||
const [details, setDetails] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.details, props.scraped.details)
|
||||
);
|
||||
|
||||
const [createTag] = useTagCreate({ name: "" });
|
||||
const Toast = useToast();
|
||||
|
@ -281,6 +302,10 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
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<IPerformerScrapeDialogProps> = (
|
|||
};
|
||||
}),
|
||||
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<IPerformerScrapeDialogProps> = (
|
|||
result={birthdate}
|
||||
onChange={(value) => setBirthdate(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Death Date"
|
||||
result={deathDate}
|
||||
onChange={(value) => setDeathDate(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Ethnicity"
|
||||
result={ethnicity}
|
||||
|
@ -380,11 +414,21 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
result={country}
|
||||
onChange={(value) => setCountry(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Hair Color"
|
||||
result={hairColor}
|
||||
onChange={(value) => setHairColor(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Eye Color"
|
||||
result={eyeColor}
|
||||
onChange={(value) => setEyeColor(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Weight"
|
||||
result={weight}
|
||||
onChange={(value) => setWeight(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Height"
|
||||
result={height}
|
||||
|
@ -430,6 +474,11 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
result={instagram}
|
||||
onChange={(value) => setInstagram(value)}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
title="Details"
|
||||
result={details}
|
||||
onChange={(value) => setDetails(value)}
|
||||
/>
|
||||
{renderScrapedTagsRow(
|
||||
tags,
|
||||
(value) => setTags(value),
|
||||
|
|
|
@ -336,8 +336,10 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
|||
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<ISceneScrapeDialogProps> = (
|
|||
try {
|
||||
performerInput = Object.assign(performerInput, toCreate);
|
||||
const result = await createPerformer({
|
||||
variables: performerInput,
|
||||
variables: { input: performerInput },
|
||||
});
|
||||
|
||||
// add the new performer to the new performers value
|
||||
|
|
|
@ -381,7 +381,7 @@ export const PerformerSelect: React.FC<IFilterProps> = (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" };
|
||||
};
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ export const Studio: React.FC = () => {
|
|||
const [name, setName] = useState<string>();
|
||||
const [url, setUrl] = useState<string>();
|
||||
const [parentStudioId, setParentStudioId] = useState<string>();
|
||||
const [details, setDetails] = useState<string>();
|
||||
|
||||
// Studio state
|
||||
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
|
||||
|
@ -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<GQL.StudioDataFragment>) {
|
||||
|
@ -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,
|
||||
})}
|
||||
<tr>
|
||||
<td>Parent Studio</td>
|
||||
<td>{renderStudio()}</td>
|
||||
|
|
|
@ -85,6 +85,13 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
|||
text={performer.birthdate ?? "Unknown"}
|
||||
/>
|
||||
</div>
|
||||
<div className="row no-gutters">
|
||||
<strong className="col-6">Death Date:</strong>
|
||||
<TruncatedText
|
||||
className="col-6"
|
||||
text={performer.death_date ?? "Unknown"}
|
||||
/>
|
||||
</div>
|
||||
<div className="row no-gutters">
|
||||
<strong className="col-6">Ethnicity:</strong>
|
||||
<TruncatedText
|
||||
|
@ -96,6 +103,13 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
|||
<strong className="col-6">Country:</strong>
|
||||
<TruncatedText className="col-6" text={performer.country ?? ""} />
|
||||
</div>
|
||||
<div className="row no-gutters">
|
||||
<strong className="col-6">Hair Color:</strong>
|
||||
<TruncatedText
|
||||
className="col-6 text-capitalize"
|
||||
text={performer.hair_color}
|
||||
/>
|
||||
</div>
|
||||
<div className="row no-gutters">
|
||||
<strong className="col-6">Eye Color:</strong>
|
||||
<TruncatedText
|
||||
|
@ -107,6 +121,10 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
|||
<strong className="col-6">Height:</strong>
|
||||
<TruncatedText className="col-6" text={performer.height} />
|
||||
</div>
|
||||
<div className="row no-gutters">
|
||||
<strong className="col-6">Weight:</strong>
|
||||
<TruncatedText className="col-6" text={performer.weight} />
|
||||
</div>
|
||||
<div className="row no-gutters">
|
||||
<strong className="col-6">Measurements:</strong>
|
||||
<TruncatedText className="col-6" text={performer.measurements} />
|
||||
|
|
|
@ -222,6 +222,10 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||
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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue