mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
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
62 changed files with 748 additions and 132 deletions
|
|
@ -31,4 +31,8 @@ fragment PerformerData on Performer {
|
||||||
stash_id
|
stash_id
|
||||||
endpoint
|
endpoint
|
||||||
}
|
}
|
||||||
|
details
|
||||||
|
death_date
|
||||||
|
hair_color
|
||||||
|
weight
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ fragment ScrapedPerformerData on ScrapedPerformer {
|
||||||
...ScrapedSceneTagData
|
...ScrapedSceneTagData
|
||||||
}
|
}
|
||||||
image
|
image
|
||||||
|
details
|
||||||
|
death_date
|
||||||
|
hair_color
|
||||||
|
weight
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment ScrapedScenePerformerData on ScrapedScenePerformer {
|
fragment ScrapedScenePerformerData on ScrapedScenePerformer {
|
||||||
|
|
@ -44,6 +48,10 @@ fragment ScrapedScenePerformerData on ScrapedScenePerformer {
|
||||||
}
|
}
|
||||||
remote_site_id
|
remote_site_id
|
||||||
images
|
images
|
||||||
|
details
|
||||||
|
death_date
|
||||||
|
hair_color
|
||||||
|
weight
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment ScrapedMovieStudioData on ScrapedMovieStudio {
|
fragment ScrapedMovieStudioData on ScrapedMovieStudio {
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,5 @@ fragment SlimStudioData on Studio {
|
||||||
parent_studio {
|
parent_studio {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
details
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,5 @@ fragment StudioData on Studio {
|
||||||
stash_id
|
stash_id
|
||||||
endpoint
|
endpoint
|
||||||
}
|
}
|
||||||
|
details
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,7 @@
|
||||||
mutation PerformerCreate(
|
mutation PerformerCreate(
|
||||||
$name: String!,
|
$input: PerformerCreateInput!) {
|
||||||
$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) {
|
|
||||||
|
|
||||||
performerCreate(input: {
|
performerCreate(input: $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
|
|
||||||
}) {
|
|
||||||
...PerformerData
|
...PerformerData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
mutation StudioCreate(
|
mutation StudioCreate(
|
||||||
$name: String!,
|
$input: StudioCreateInput!) {
|
||||||
$url: String,
|
|
||||||
$image: String,
|
|
||||||
$stash_ids: [StashIDInput!],
|
|
||||||
$parent_id: ID) {
|
|
||||||
|
|
||||||
studioCreate(input: { name: $name, url: $url, image: $image, stash_ids: $stash_ids, parent_id: $parent_id }) {
|
studioCreate(input: $input) {
|
||||||
...StudioData
|
...StudioData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ query ScrapeFreeones($performer_name: String!) {
|
||||||
tattoos
|
tattoos
|
||||||
piercings
|
piercings
|
||||||
aliases
|
aliases
|
||||||
|
details
|
||||||
|
death_date
|
||||||
|
hair_color
|
||||||
|
weight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,12 @@ input PerformerFilterType {
|
||||||
stash_id: String
|
stash_id: String
|
||||||
"""Filter by url"""
|
"""Filter by url"""
|
||||||
url: StringCriterionInput
|
url: StringCriterionInput
|
||||||
|
"""Filter by hair color"""
|
||||||
|
hair_color: StringCriterionInput
|
||||||
|
"""Filter by weight"""
|
||||||
|
weight: StringCriterionInput
|
||||||
|
"""Filter by death year"""
|
||||||
|
death_year: IntCriterionInput
|
||||||
}
|
}
|
||||||
|
|
||||||
input SceneMarkerFilterType {
|
input SceneMarkerFilterType {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@ type Performer {
|
||||||
gallery_count: Int # Resolver
|
gallery_count: Int # Resolver
|
||||||
scenes: [Scene!]!
|
scenes: [Scene!]!
|
||||||
stash_ids: [StashID!]!
|
stash_ids: [StashID!]!
|
||||||
|
details: String
|
||||||
|
death_date: String
|
||||||
|
hair_color: String
|
||||||
|
weight: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
input PerformerCreateInput {
|
input PerformerCreateInput {
|
||||||
|
|
@ -59,6 +63,10 @@ input PerformerCreateInput {
|
||||||
"""This should be a URL or a base64 encoded data URL"""
|
"""This should be a URL or a base64 encoded data URL"""
|
||||||
image: String
|
image: String
|
||||||
stash_ids: [StashIDInput!]
|
stash_ids: [StashIDInput!]
|
||||||
|
details: String
|
||||||
|
death_date: String
|
||||||
|
hair_color: String
|
||||||
|
weight: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
input PerformerUpdateInput {
|
input PerformerUpdateInput {
|
||||||
|
|
@ -84,6 +92,10 @@ input PerformerUpdateInput {
|
||||||
"""This should be a URL or a base64 encoded data URL"""
|
"""This should be a URL or a base64 encoded data URL"""
|
||||||
image: String
|
image: String
|
||||||
stash_ids: [StashIDInput!]
|
stash_ids: [StashIDInput!]
|
||||||
|
details: String
|
||||||
|
death_date: String
|
||||||
|
hair_color: String
|
||||||
|
weight: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
input BulkPerformerUpdateInput {
|
input BulkPerformerUpdateInput {
|
||||||
|
|
@ -106,6 +118,10 @@ input BulkPerformerUpdateInput {
|
||||||
instagram: String
|
instagram: String
|
||||||
favorite: Boolean
|
favorite: Boolean
|
||||||
tag_ids: BulkUpdateIds
|
tag_ids: BulkUpdateIds
|
||||||
|
details: String
|
||||||
|
death_date: String
|
||||||
|
hair_color: String
|
||||||
|
weight: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
input PerformerDestroyInput {
|
input PerformerDestroyInput {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ type ScrapedPerformer {
|
||||||
|
|
||||||
"""This should be a base64 encoded data URL"""
|
"""This should be a base64 encoded data URL"""
|
||||||
image: String
|
image: String
|
||||||
|
details: String
|
||||||
|
death_date: String
|
||||||
|
hair_color: String
|
||||||
|
weight: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input ScrapedPerformerInput {
|
input ScrapedPerformerInput {
|
||||||
|
|
@ -43,4 +47,8 @@ input ScrapedPerformerInput {
|
||||||
|
|
||||||
# not including tags for the input
|
# not including tags for the input
|
||||||
# not including image 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
|
remote_site_id: String
|
||||||
images: [String!]
|
images: [String!]
|
||||||
|
details: String
|
||||||
|
death_date: String
|
||||||
|
hair_color: String
|
||||||
|
weight: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrapedSceneMovie {
|
type ScrapedSceneMovie {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ type Studio {
|
||||||
image_count: Int # Resolver
|
image_count: Int # Resolver
|
||||||
gallery_count: Int # Resolver
|
gallery_count: Int # Resolver
|
||||||
stash_ids: [StashID!]!
|
stash_ids: [StashID!]!
|
||||||
|
details: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input StudioCreateInput {
|
input StudioCreateInput {
|
||||||
|
|
@ -20,6 +21,7 @@ input StudioCreateInput {
|
||||||
"""This should be a URL or a base64 encoded data URL"""
|
"""This should be a URL or a base64 encoded data URL"""
|
||||||
image: String
|
image: String
|
||||||
stash_ids: [StashIDInput!]
|
stash_ids: [StashIDInput!]
|
||||||
|
details: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input StudioUpdateInput {
|
input StudioUpdateInput {
|
||||||
|
|
@ -30,6 +32,7 @@ input StudioUpdateInput {
|
||||||
"""This should be a URL or a base64 encoded data URL"""
|
"""This should be a URL or a base64 encoded data URL"""
|
||||||
image: String
|
image: String
|
||||||
stash_ids: [StashIDInput!]
|
stash_ids: [StashIDInput!]
|
||||||
|
details: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input StudioDestroyInput {
|
input StudioDestroyInput {
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,11 @@ fragment PerformerFragment on Performer {
|
||||||
piercings {
|
piercings {
|
||||||
...BodyModificationFragment
|
...BodyModificationFragment
|
||||||
}
|
}
|
||||||
|
details
|
||||||
|
death_date {
|
||||||
|
...FuzzyDateFragment
|
||||||
|
}
|
||||||
|
weight
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment PerformerAppearanceFragment on PerformerAppearance {
|
fragment PerformerAppearanceFragment on PerformerAppearance {
|
||||||
|
|
|
||||||
|
|
@ -208,3 +208,32 @@ func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer)
|
||||||
|
|
||||||
return ret, nil
|
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
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/performer"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -83,6 +85,25 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||||
} else {
|
} else {
|
||||||
newPerformer.Favorite = sql.NullBool{Bool: false, Valid: true}
|
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
|
// Start the transaction and save the performer
|
||||||
var performer *models.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.Twitter = translator.nullString(input.Twitter, "twitter")
|
||||||
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
|
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
|
||||||
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
|
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
|
// Start the transaction and save the p
|
||||||
var performer *models.Performer
|
var p *models.Performer
|
||||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||||
qb := repo.Performer()
|
qb := repo.Performer()
|
||||||
|
|
||||||
var err error
|
// need to get existing performer
|
||||||
performer, err = qb.Update(updatedPerformer)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the tags
|
// Save the tags
|
||||||
if translator.hasField("tag_ids") {
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update image table
|
// update image table
|
||||||
if len(imageData) > 0 {
|
if len(imageData) > 0 {
|
||||||
if err := qb.UpdateImage(performer.ID, imageData); err != nil {
|
if err := qb.UpdateImage(p.ID, imageData); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if imageIncluded {
|
} else if imageIncluded {
|
||||||
// must be unsetting
|
// must be unsetting
|
||||||
if err := qb.DestroyImage(performer.ID); err != nil {
|
if err := qb.DestroyImage(p.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -221,7 +261,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return performer, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) updatePerformerTags(qb models.PerformerReaderWriter, performerID int, tagsIDs []string) error {
|
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.Twitter = translator.nullString(input.Twitter, "twitter")
|
||||||
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
|
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
|
||||||
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
|
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 translator.hasField("gender") {
|
||||||
if input.Gender != nil {
|
if input.Gender != nil {
|
||||||
|
|
@ -282,6 +326,20 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models
|
||||||
for _, performerID := range performerIDs {
|
for _, performerID := range performerIDs {
|
||||||
updatedPerformer.ID = performerID
|
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)
|
performer, err := qb.Update(updatedPerformer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,10 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||||
newStudio.ParentID = sql.NullInt64{Int64: parentID, Valid: true}
|
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
|
// Start the transaction and save the studio
|
||||||
var studio *models.Studio
|
var studio *models.Studio
|
||||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
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.URL = translator.nullString(input.URL, "url")
|
||||||
|
updatedStudio.Details = translator.nullString(input.Details, "details")
|
||||||
updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id")
|
updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id")
|
||||||
|
|
||||||
// Start the transaction and save the studio
|
// Start the transaction and save the studio
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import (
|
||||||
var DB *sqlx.DB
|
var DB *sqlx.DB
|
||||||
var WriteMu *sync.Mutex
|
var WriteMu *sync.Mutex
|
||||||
var dbPath string
|
var dbPath string
|
||||||
var appSchemaVersion uint = 20
|
var appSchemaVersion uint = 21
|
||||||
var databaseSchemaVersion uint
|
var databaseSchemaVersion uint
|
||||||
|
|
||||||
var (
|
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"`
|
Image string `json:"image,omitempty"`
|
||||||
CreatedAt models.JSONTime `json:"created_at,omitempty"`
|
CreatedAt models.JSONTime `json:"created_at,omitempty"`
|
||||||
UpdatedAt models.JSONTime `json:"updated_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) {
|
func LoadPerformerFile(filePath string) (*Performer, error) {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ type Studio struct {
|
||||||
Image string `json:"image,omitempty"`
|
Image string `json:"image,omitempty"`
|
||||||
CreatedAt models.JSONTime `json:"created_at,omitempty"`
|
CreatedAt models.JSONTime `json:"created_at,omitempty"`
|
||||||
UpdatedAt models.JSONTime `json:"updated_at,omitempty"`
|
UpdatedAt models.JSONTime `json:"updated_at,omitempty"`
|
||||||
|
Details string `json:"details,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadStudioFile(filePath string) (*Studio, error) {
|
func LoadStudioFile(filePath string) (*Studio, error) {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ type Performer struct {
|
||||||
Favorite sql.NullBool `db:"favorite" json:"favorite"`
|
Favorite sql.NullBool `db:"favorite" json:"favorite"`
|
||||||
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_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 {
|
type PerformerPartial struct {
|
||||||
|
|
@ -53,6 +57,10 @@ type PerformerPartial struct {
|
||||||
Favorite *sql.NullBool `db:"favorite" json:"favorite"`
|
Favorite *sql.NullBool `db:"favorite" json:"favorite"`
|
||||||
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
|
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_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 {
|
func NewPerformer(name string) *Performer {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,10 @@ type ScrapedPerformer struct {
|
||||||
Aliases *string `graphql:"aliases" json:"aliases"`
|
Aliases *string `graphql:"aliases" json:"aliases"`
|
||||||
Tags []*ScrapedSceneTag `graphql:"tags" json:"tags"`
|
Tags []*ScrapedSceneTag `graphql:"tags" json:"tags"`
|
||||||
Image *string `graphql:"image" json:"image"`
|
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
|
// this type has no Image field
|
||||||
|
|
@ -63,6 +67,10 @@ type ScrapedPerformerStash struct {
|
||||||
Piercings *string `graphql:"piercings" json:"piercings"`
|
Piercings *string `graphql:"piercings" json:"piercings"`
|
||||||
Aliases *string `graphql:"aliases" json:"aliases"`
|
Aliases *string `graphql:"aliases" json:"aliases"`
|
||||||
Tags []*ScrapedSceneTag `graphql:"tags" json:"tags"`
|
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 {
|
type ScrapedScene struct {
|
||||||
|
|
@ -128,6 +136,10 @@ type ScrapedScenePerformer struct {
|
||||||
Tags []*ScrapedSceneTag `graphql:"tags" json:"tags"`
|
Tags []*ScrapedSceneTag `graphql:"tags" json:"tags"`
|
||||||
RemoteSiteID *string `graphql:"remote_site_id" json:"remote_site_id"`
|
RemoteSiteID *string `graphql:"remote_site_id" json:"remote_site_id"`
|
||||||
Images []string `graphql:"images" json:"images"`
|
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 {
|
type ScrapedSceneStudio struct {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ type Studio struct {
|
||||||
ParentID sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
ParentID sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
||||||
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||||
|
Details sql.NullString `db:"details" json:"details"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StudioPartial struct {
|
type StudioPartial struct {
|
||||||
|
|
@ -25,6 +26,7 @@ type StudioPartial struct {
|
||||||
ParentID *sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
ParentID *sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
||||||
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
|
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_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"
|
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 {
|
if performer.Favorite.Valid {
|
||||||
newPerformerJSON.Favorite = performer.Favorite.Bool
|
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)
|
image, err := reader.GetImage(performer.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ const (
|
||||||
piercings = "piercings"
|
piercings = "piercings"
|
||||||
tattoos = "tattoos"
|
tattoos = "tattoos"
|
||||||
twitter = "twitter"
|
twitter = "twitter"
|
||||||
|
details = "details"
|
||||||
|
hairColor = "hairColor"
|
||||||
|
weight = 60
|
||||||
)
|
)
|
||||||
|
|
||||||
var imageBytes = []byte("imageBytes")
|
var imageBytes = []byte("imageBytes")
|
||||||
|
|
@ -46,6 +49,10 @@ var birthDate = models.SQLiteDate{
|
||||||
String: "2001-01-01",
|
String: "2001-01-01",
|
||||||
Valid: true,
|
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 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)
|
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{
|
UpdatedAt: models.SQLiteTimestamp{
|
||||||
Timestamp: updateTime,
|
Timestamp: updateTime,
|
||||||
},
|
},
|
||||||
|
Details: models.NullString(details),
|
||||||
|
DeathDate: deathDate,
|
||||||
|
HairColor: models.NullString(hairColor),
|
||||||
|
Weight: sql.NullInt64{
|
||||||
|
Int64: weight,
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,6 +134,10 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
|
||||||
Time: updateTime,
|
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 != "" {
|
if performerJSON.Instagram != "" {
|
||||||
newPerformer.Instagram = sql.NullString{String: performerJSON.Instagram, Valid: true}
|
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
|
return newPerformer
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
pkg/performer/validate.go
Normal file
37
pkg/performer/validate.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
70
pkg/performer/validate_test.go
Normal file
70
pkg/performer/validate_test.go
Normal file
|
|
@ -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
|
selector: //div[contains(@class,'image-container')]//a/img/@src
|
||||||
Gender:
|
Gender:
|
||||||
fixed: "Female"
|
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 {
|
func getFreeonesScraper() config {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ jsonScrapers:
|
||||||
Piercings: $extras.piercings
|
Piercings: $extras.piercings
|
||||||
Aliases: data.aliases
|
Aliases: data.aliases
|
||||||
Image: data.image
|
Image: data.image
|
||||||
|
Details: data.bio
|
||||||
|
HairColor: $extras.hair_colour
|
||||||
|
Weight: $extras.weight
|
||||||
`
|
`
|
||||||
|
|
||||||
const json = `
|
const json = `
|
||||||
|
|
@ -41,7 +44,7 @@ jsonScrapers:
|
||||||
"ethnicity": "Caucasian",
|
"ethnicity": "Caucasian",
|
||||||
"nationality": "United States",
|
"nationality": "United States",
|
||||||
"hair_colour": "Blonde",
|
"hair_colour": "Blonde",
|
||||||
"weight": "126 lbs (or 57 kg)",
|
"weight": 57,
|
||||||
"height": "5'6\" (or 167 cm)",
|
"height": "5'6\" (or 167 cm)",
|
||||||
"measurements": "34-26-36",
|
"measurements": "34-26-36",
|
||||||
"cupsize": "34C (75C)",
|
"cupsize": "34C (75C)",
|
||||||
|
|
@ -90,4 +93,7 @@ jsonScrapers:
|
||||||
verifyField(t, "5'6\" (or 167 cm)", scrapedPerformer.Height, "Height")
|
verifyField(t, "5'6\" (or 167 cm)", scrapedPerformer.Height, "Height")
|
||||||
verifyField(t, "None", scrapedPerformer.Tattoos, "Tattoos")
|
verifyField(t, "None", scrapedPerformer.Tattoos, "Tattoos")
|
||||||
verifyField(t, "Navel", scrapedPerformer.Piercings, "Piercings")
|
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
|
5ft7
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="paramname">
|
||||||
|
<b>Weight:</b>
|
||||||
|
</td>
|
||||||
|
<td class="paramvalue">
|
||||||
|
57
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="paramname">
|
<td class="paramname">
|
||||||
<b>Measurements:</b>
|
<b>Measurements:</b>
|
||||||
|
|
@ -141,6 +149,14 @@ const htmlDoc1 = `
|
||||||
<!-- None -->;
|
<!-- None -->;
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
<tr>
|
||||||
<td class="paramname">
|
<td class="paramname">
|
||||||
<div><b>Social Network Links:</b></div>
|
<div><b>Social Network Links:</b></div>
|
||||||
|
|
@ -194,6 +210,9 @@ func makeXPathConfig() mappedPerformerScraperConfig {
|
||||||
config.mappedConfig["FakeTits"] = makeSimpleAttrConfig(makeCommonXPath("Fake boobs:"))
|
config.mappedConfig["FakeTits"] = makeSimpleAttrConfig(makeCommonXPath("Fake boobs:"))
|
||||||
config.mappedConfig["Tattoos"] = makeSimpleAttrConfig(makeCommonXPath("Tattoos:"))
|
config.mappedConfig["Tattoos"] = makeSimpleAttrConfig(makeCommonXPath("Tattoos:"))
|
||||||
config.mappedConfig["Piercings"] = makeSimpleAttrConfig(makeCommonXPath("Piercings:") + "/comment()")
|
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
|
// special handling for birthdate
|
||||||
birthdateAttrConfig := makeSimpleAttrConfig(makeCommonXPath("Date of Birth:"))
|
birthdateAttrConfig := makeSimpleAttrConfig(makeCommonXPath("Date of Birth:"))
|
||||||
|
|
@ -299,6 +318,9 @@ func TestScrapePerformerXPath(t *testing.T) {
|
||||||
const piercings = "<!-- None -->"
|
const piercings = "<!-- None -->"
|
||||||
const gender = "Female"
|
const gender = "Female"
|
||||||
const height = "170"
|
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, performerName, performer.Name, "Name")
|
||||||
verifyField(t, gender, performer.Gender, "Gender")
|
verifyField(t, gender, performer.Gender, "Gender")
|
||||||
|
|
@ -317,6 +339,9 @@ func TestScrapePerformerXPath(t *testing.T) {
|
||||||
verifyField(t, tattoos, performer.Tattoos, "Tattoos")
|
verifyField(t, tattoos, performer.Tattoos, "Tattoos")
|
||||||
verifyField(t, piercings, performer.Piercings, "Piercings")
|
verifyField(t, piercings, performer.Piercings, "Piercings")
|
||||||
verifyField(t, height, performer.Height, "Height")
|
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) {
|
func TestConcatXPath(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
@ -209,7 +208,13 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy
|
||||||
}
|
}
|
||||||
|
|
||||||
if birthYear := performerFilter.BirthYear; birthYear != nil {
|
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.addWhere(clauses...)
|
||||||
query.addArg(thisArgs...)
|
query.addArg(thisArgs...)
|
||||||
}
|
}
|
||||||
|
|
@ -254,6 +259,8 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy
|
||||||
query.handleStringCriterionInput(performerFilter.CareerLength, tableName+".career_length")
|
query.handleStringCriterionInput(performerFilter.CareerLength, tableName+".career_length")
|
||||||
query.handleStringCriterionInput(performerFilter.Tattoos, tableName+".tattoos")
|
query.handleStringCriterionInput(performerFilter.Tattoos, tableName+".tattoos")
|
||||||
query.handleStringCriterionInput(performerFilter.Piercings, tableName+".piercings")
|
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")
|
query.handleStringCriterionInput(performerFilter.URL, tableName+".url")
|
||||||
|
|
||||||
// TODO - need better handling of aliases
|
// TODO - need better handling of aliases
|
||||||
|
|
@ -294,7 +301,7 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy
|
||||||
return performers, countResult, nil
|
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 clauses []string
|
||||||
var args []interface{}
|
var args []interface{}
|
||||||
|
|
||||||
|
|
@ -306,22 +313,22 @@ func getBirthYearFilterClause(criterionModifier models.CriterionModifier, value
|
||||||
switch modifier {
|
switch modifier {
|
||||||
case "EQUALS":
|
case "EQUALS":
|
||||||
// between yyyy-01-01 and yyyy-12-31
|
// between yyyy-01-01 and yyyy-12-31
|
||||||
clauses = append(clauses, "performers.birthdate >= ?")
|
clauses = append(clauses, "performers."+col+" >= ?")
|
||||||
clauses = append(clauses, "performers.birthdate <= ?")
|
clauses = append(clauses, "performers."+col+" <= ?")
|
||||||
args = append(args, startOfYear)
|
args = append(args, startOfYear)
|
||||||
args = append(args, endOfYear)
|
args = append(args, endOfYear)
|
||||||
case "NOT_EQUALS":
|
case "NOT_EQUALS":
|
||||||
// outside of yyyy-01-01 to yyyy-12-31
|
// 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, startOfYear)
|
||||||
args = append(args, endOfYear)
|
args = append(args, endOfYear)
|
||||||
case "GREATER_THAN":
|
case "GREATER_THAN":
|
||||||
// > yyyy-12-31
|
// > yyyy-12-31
|
||||||
clauses = append(clauses, "performers.birthdate > ?")
|
clauses = append(clauses, "performers."+col+" > ?")
|
||||||
args = append(args, endOfYear)
|
args = append(args, endOfYear)
|
||||||
case "LESS_THAN":
|
case "LESS_THAN":
|
||||||
// < yyyy-01-01
|
// < yyyy-01-01
|
||||||
clauses = append(clauses, "performers.birthdate < ?")
|
clauses = append(clauses, "performers."+col+" < ?")
|
||||||
args = append(args, startOfYear)
|
args = append(args, startOfYear)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -332,33 +339,23 @@ func getBirthYearFilterClause(criterionModifier models.CriterionModifier, value
|
||||||
func getAgeFilterClause(criterionModifier models.CriterionModifier, value int) ([]string, []interface{}) {
|
func getAgeFilterClause(criterionModifier models.CriterionModifier, value int) ([]string, []interface{}) {
|
||||||
var clauses []string
|
var clauses []string
|
||||||
var args []interface{}
|
var args []interface{}
|
||||||
|
var clause string
|
||||||
|
|
||||||
// get the date at which performer would turn the age specified
|
if criterionModifier.IsValid() {
|
||||||
dt := time.Now()
|
switch criterionModifier {
|
||||||
birthDate := dt.AddDate(-value-1, 0, 0)
|
case models.CriterionModifierEquals:
|
||||||
yearAfter := birthDate.AddDate(1, 0, 0)
|
clause = " == ?"
|
||||||
|
case models.CriterionModifierNotEquals:
|
||||||
|
clause = " != ?"
|
||||||
|
case models.CriterionModifierGreaterThan:
|
||||||
|
clause = " > ?"
|
||||||
|
case models.CriterionModifierLessThan:
|
||||||
|
clause = " < ?"
|
||||||
|
}
|
||||||
|
|
||||||
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
|
if clause != "" {
|
||||||
switch modifier {
|
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)
|
||||||
case "EQUALS":
|
args = append(args, value)
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,10 +214,16 @@ func verifyPerformerAge(t *testing.T, ageCriterion models.IntCriterionInput) {
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for _, performer := range performers {
|
for _, performer := range performers {
|
||||||
|
cd := now
|
||||||
|
|
||||||
|
if performer.DeathDate.Valid {
|
||||||
|
cd, _ = time.Parse("2006-01-02", performer.DeathDate.String)
|
||||||
|
}
|
||||||
|
|
||||||
bd := performer.Birthdate.String
|
bd := performer.Birthdate.String
|
||||||
d, _ := time.Parse("2006-01-02", bd)
|
d, _ := time.Parse("2006-01-02", bd)
|
||||||
age := now.Year() - d.Year()
|
age := cd.Year() - d.Year()
|
||||||
if now.YearDay() < d.YearDay() {
|
if cd.YearDay() < d.YearDay() {
|
||||||
age = age - 1
|
age = age - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -558,10 +558,10 @@ func verifyInt(t *testing.T, value int, criterion models.IntCriterionInput) {
|
||||||
assert.NotEqual(criterion.Value, value)
|
assert.NotEqual(criterion.Value, value)
|
||||||
}
|
}
|
||||||
if criterion.Modifier == models.CriterionModifierGreaterThan {
|
if criterion.Modifier == models.CriterionModifierGreaterThan {
|
||||||
assert.True(value > criterion.Value)
|
assert.Greater(value, criterion.Value)
|
||||||
}
|
}
|
||||||
if criterion.Modifier == models.CriterionModifierLessThan {
|
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")
|
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 {
|
func getPerformerCareerLength(index int) *string {
|
||||||
if index%5 == 0 {
|
if index%5 == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -691,6 +704,8 @@ func createPerformers(pqb models.PerformerReaderWriter, n int, o int) error {
|
||||||
String: getPerformerBirthdate(i),
|
String: getPerformerBirthdate(i),
|
||||||
Valid: true,
|
Valid: true,
|
||||||
},
|
},
|
||||||
|
DeathDate: getPerformerDeathDate(i),
|
||||||
|
Details: sql.NullString{String: getPerformerStringValue(i, "Details"), Valid: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
careerLength := getPerformerCareerLength(i)
|
careerLength := getPerformerCareerLength(i)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@ func ToJSON(reader models.StudioReader, studio *models.Studio) (*jsonschema.Stud
|
||||||
newStudioJSON.URL = studio.URL.String
|
newStudioJSON.URL = studio.URL.String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if studio.Details.Valid {
|
||||||
|
newStudioJSON.Details = studio.Details.String
|
||||||
|
}
|
||||||
|
|
||||||
if studio.ParentID.Valid {
|
if studio.ParentID.Valid {
|
||||||
parent, err := reader.Find(int(studio.ParentID.Int64))
|
parent, err := reader.Find(int(studio.ParentID.Int64))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,13 @@ const (
|
||||||
errParentStudioID = 12
|
errParentStudioID = 12
|
||||||
)
|
)
|
||||||
|
|
||||||
const studioName = "testStudio"
|
const (
|
||||||
const url = "url"
|
studioName = "testStudio"
|
||||||
|
url = "url"
|
||||||
|
details = "details"
|
||||||
|
|
||||||
const parentStudioName = "parentStudio"
|
parentStudioName = "parentStudio"
|
||||||
|
)
|
||||||
|
|
||||||
var parentStudio models.Studio = models.Studio{
|
var parentStudio models.Studio = models.Studio{
|
||||||
Name: models.NullString(parentStudioName),
|
Name: models.NullString(parentStudioName),
|
||||||
|
|
@ -37,15 +40,15 @@ var imageBytes = []byte("imageBytes")
|
||||||
|
|
||||||
const image = "aW1hZ2VCeXRlcw=="
|
const image = "aW1hZ2VCeXRlcw=="
|
||||||
|
|
||||||
var createTime time.Time = time.Date(2001, 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.UTC)
|
var updateTime time.Time = time.Date(2002, 01, 01, 0, 0, 0, 0, time.Local)
|
||||||
|
|
||||||
func createFullStudio(id int, parentID int) models.Studio {
|
func createFullStudio(id int, parentID int) models.Studio {
|
||||||
return models.Studio{
|
ret := models.Studio{
|
||||||
ID: id,
|
ID: id,
|
||||||
Name: models.NullString(studioName),
|
Name: models.NullString(studioName),
|
||||||
URL: models.NullString(url),
|
URL: models.NullString(url),
|
||||||
ParentID: models.NullInt64(int64(parentID)),
|
Details: models.NullString(details),
|
||||||
CreatedAt: models.SQLiteTimestamp{
|
CreatedAt: models.SQLiteTimestamp{
|
||||||
Timestamp: createTime,
|
Timestamp: createTime,
|
||||||
},
|
},
|
||||||
|
|
@ -53,6 +56,12 @@ func createFullStudio(id int, parentID int) models.Studio {
|
||||||
Timestamp: updateTime,
|
Timestamp: updateTime,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if parentID != 0 {
|
||||||
|
ret.ParentID = models.NullInt64(int64(parentID))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func createEmptyStudio(id int) models.Studio {
|
func createEmptyStudio(id int) models.Studio {
|
||||||
|
|
@ -71,6 +80,7 @@ func createFullJSONStudio(parentStudio, image string) *jsonschema.Studio {
|
||||||
return &jsonschema.Studio{
|
return &jsonschema.Studio{
|
||||||
Name: studioName,
|
Name: studioName,
|
||||||
URL: url,
|
URL: url,
|
||||||
|
Details: details,
|
||||||
CreatedAt: models.JSONTime{
|
CreatedAt: models.JSONTime{
|
||||||
Time: createTime,
|
Time: createTime,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ func (i *Importer) PreImport() error {
|
||||||
Checksum: checksum,
|
Checksum: checksum,
|
||||||
Name: sql.NullString{String: i.Input.Name, Valid: true},
|
Name: sql.NullString{String: i.Input.Name, Valid: true},
|
||||||
URL: sql.NullString{String: i.Input.URL, 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()},
|
CreatedAt: models.SQLiteTimestamp{Timestamp: i.Input.CreatedAt.GetTime()},
|
||||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: i.Input.UpdatedAt.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/manager/jsonschema"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/mocks"
|
"github.com/stashapp/stash/pkg/models/mocks"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
|
|
@ -51,6 +52,17 @@ func TestImporterPreImport(t *testing.T) {
|
||||||
err = i.PreImport()
|
err = i.PreImport()
|
||||||
|
|
||||||
assert.Nil(t, err)
|
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) {
|
func TestImporterPreImportWithParent(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Added details, death date, hair color, and weight to Performers.
|
||||||
|
* Added details to Studios.
|
||||||
* Added [perceptual dupe checker](/settings?tab=duplicates).
|
* 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.
|
* Support access to system without logging in via API key.
|
||||||
* Added scene queue.
|
* Added scene queue.
|
||||||
|
|
||||||
|
|
@ -10,12 +16,8 @@
|
||||||
* Add slideshow to image wall view.
|
* Add slideshow to image wall view.
|
||||||
* Support API key via URL query parameter, and added API key to stream link in Scene File Info.
|
* Support API key via URL query parameter, and added API key to stream link in Scene File Info.
|
||||||
* Revamped setup wizard and migration UI.
|
* Revamped setup wizard and migration UI.
|
||||||
* Add various `count` filter criteria and sort options.
|
|
||||||
* Scroll to top when changing page number.
|
* 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.
|
* 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.
|
* Disable sounds on scene/marker wall previews by default.
|
||||||
* Improve Movie UI.
|
* Improve Movie UI.
|
||||||
* Change performer text query to search by name and alias only.
|
* Change performer text query to search by name and alias only.
|
||||||
|
|
|
||||||
|
|
@ -273,9 +273,11 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||||
try {
|
try {
|
||||||
const result = await createStudio({
|
const result = await createStudio({
|
||||||
variables: {
|
variables: {
|
||||||
|
input: {
|
||||||
name: toCreate.name,
|
name: toCreate.name,
|
||||||
url: toCreate.url,
|
url: toCreate.url,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// set the new studio as the value
|
// set the new studio as the value
|
||||||
|
|
@ -299,7 +301,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||||
try {
|
try {
|
||||||
performerInput = Object.assign(performerInput, toCreate);
|
performerInput = Object.assign(performerInput, toCreate);
|
||||||
const result = await createPerformer({
|
const result = await createPerformer({
|
||||||
variables: performerInput,
|
variables: { input: performerInput },
|
||||||
});
|
});
|
||||||
|
|
||||||
// add the new performer to the new performers value
|
// add the new performer to the new performers value
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,10 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
||||||
selected,
|
selected,
|
||||||
onSelectedChanged,
|
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." : "."}`;
|
const ageString = `${age} years old${ageFromDate ? " in this scene." : "."}`;
|
||||||
|
|
||||||
function maybeRenderFavoriteBanner() {
|
function maybeRenderFavoriteBanner() {
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,9 @@ export const Performer: React.FC = () => {
|
||||||
// provided by the server
|
// provided by the server
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
<span className="age-tail"> years old</span>
|
||||||
</div>
|
</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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
|
|
@ -91,15 +102,22 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||||
name="Birthdate"
|
name="Birthdate"
|
||||||
value={TextUtils.formatDate(intl, performer.birthdate ?? undefined)}
|
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="Ethnicity" value={performer.ethnicity} />
|
||||||
|
<TextField name="Hair Color" value={performer.hair_color} />
|
||||||
<TextField name="Eye Color" value={performer.eye_color} />
|
<TextField name="Eye Color" value={performer.eye_color} />
|
||||||
<TextField name="Country" value={performer.country} />
|
<TextField name="Country" value={performer.country} />
|
||||||
<TextField name="Height" value={formatHeight(performer.height)} />
|
<TextField name="Height" value={formatHeight(performer.height)} />
|
||||||
|
<TextField name="Weight" value={formatWeight(performer.weight)} />
|
||||||
<TextField name="Measurements" value={performer.measurements} />
|
<TextField name="Measurements" value={performer.measurements} />
|
||||||
<TextField name="Fake Tits" value={performer.fake_tits} />
|
<TextField name="Fake Tits" value={performer.fake_tits} />
|
||||||
<TextField name="Career Length" value={performer.career_length} />
|
<TextField name="Career Length" value={performer.career_length} />
|
||||||
<TextField name="Tattoos" value={performer.tattoos} />
|
<TextField name="Tattoos" value={performer.tattoos} />
|
||||||
<TextField name="Piercings" value={performer.piercings} />
|
<TextField name="Piercings" value={performer.piercings} />
|
||||||
|
<TextField name="Details" value={performer.details} />
|
||||||
<URLField
|
<URLField
|
||||||
name="URL"
|
name="URL"
|
||||||
value={performer.url}
|
value={performer.url}
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||||
tag_ids: yup.array(yup.string().required()).optional(),
|
tag_ids: yup.array(yup.string().required()).optional(),
|
||||||
stash_ids: yup.mixed<GQL.StashIdInput>().optional(),
|
stash_ids: yup.mixed<GQL.StashIdInput>().optional(),
|
||||||
image: yup.string().optional().nullable(),
|
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 = {
|
const initialValues = {
|
||||||
|
|
@ -131,6 +135,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||||
tag_ids: (performer.tags ?? []).map((t) => t.id),
|
tag_ids: (performer.tags ?? []).map((t) => t.id),
|
||||||
stash_ids: performer.stash_ids ?? undefined,
|
stash_ids: performer.stash_ids ?? undefined,
|
||||||
image: undefined,
|
image: undefined,
|
||||||
|
details: performer.details ?? "",
|
||||||
|
death_date: performer.death_date ?? "",
|
||||||
|
hair_color: performer.hair_color ?? "",
|
||||||
|
weight: performer.weight ?? "",
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputValues = typeof initialValues;
|
type InputValues = typeof initialValues;
|
||||||
|
|
@ -306,6 +314,18 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||||
const imageStr = (state as GQL.ScrapedPerformerDataFragment).image;
|
const imageStr = (state as GQL.ScrapedPerformerDataFragment).image;
|
||||||
formik.setFieldValue("image", imageStr ?? undefined);
|
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) {
|
function onImageLoad(imageData: string) {
|
||||||
|
|
@ -334,7 +354,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||||
history.push(`/performers/${performer.id}`);
|
history.push(`/performers/${performer.id}`);
|
||||||
} else {
|
} else {
|
||||||
const result = await createPerformer({
|
const result = await createPerformer({
|
||||||
variables: performerInput as GQL.PerformerCreateInput,
|
variables: { input: performerInput as GQL.PerformerCreateInput },
|
||||||
});
|
});
|
||||||
if (result.data?.performerCreate) {
|
if (result.data?.performerCreate) {
|
||||||
history.push(`/performers/${result.data.performerCreate.id}`);
|
history.push(`/performers/${result.data.performerCreate.id}`);
|
||||||
|
|
@ -399,6 +419,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||||
> = {
|
> = {
|
||||||
...values,
|
...values,
|
||||||
gender: stringToGender(values.gender),
|
gender: stringToGender(values.gender),
|
||||||
|
weight: Number(values.weight),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isNew) {
|
if (!isNew) {
|
||||||
|
|
@ -550,6 +571,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||||
...formik.values,
|
...formik.values,
|
||||||
gender: stringToGender(formik.values.gender),
|
gender: stringToGender(formik.values.gender),
|
||||||
image: formik.values.image ?? performer.image_path,
|
image: formik.values.image ?? performer.image_path,
|
||||||
|
weight: Number(formik.values.weight),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -806,10 +828,13 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
{renderTextField("birthdate", "Birthdate", "YYYY-MM-DD")}
|
{renderTextField("birthdate", "Birthdate", "YYYY-MM-DD")}
|
||||||
|
{renderTextField("death_date", "Death Date", "YYYY-MM-DD")}
|
||||||
{renderTextField("country", "Country")}
|
{renderTextField("country", "Country")}
|
||||||
{renderTextField("ethnicity", "Ethnicity")}
|
{renderTextField("ethnicity", "Ethnicity")}
|
||||||
|
{renderTextField("hair_color", "Hair Color")}
|
||||||
{renderTextField("eye_color", "Eye Color")}
|
{renderTextField("eye_color", "Eye Color")}
|
||||||
{renderTextField("height", "Height (cm)")}
|
{renderTextField("height", "Height (cm)")}
|
||||||
|
{renderTextField("weight", "Weight (kg)")}
|
||||||
{renderTextField("measurements", "Measurements")}
|
{renderTextField("measurements", "Measurements")}
|
||||||
{renderTextField("fake_tits", "Fake Tits")}
|
{renderTextField("fake_tits", "Fake Tits")}
|
||||||
|
|
||||||
|
|
@ -861,7 +886,19 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||||
|
|
||||||
{renderTextField("twitter", "Twitter")}
|
{renderTextField("twitter", "Twitter")}
|
||||||
{renderTextField("instagram", "Instagram")}
|
{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()}
|
{renderTagsField()}
|
||||||
{renderStashIDs()}
|
{renderStashIDs()}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -153,18 +153,36 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||||
const [birthdate, setBirthdate] = useState<ScrapeResult<string>>(
|
const [birthdate, setBirthdate] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(props.performer.birthdate, props.scraped.birthdate)
|
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>>(
|
const [ethnicity, setEthnicity] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(props.performer.ethnicity, props.scraped.ethnicity)
|
new ScrapeResult<string>(props.performer.ethnicity, props.scraped.ethnicity)
|
||||||
);
|
);
|
||||||
const [country, setCountry] = useState<ScrapeResult<string>>(
|
const [country, setCountry] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(props.performer.country, props.scraped.country)
|
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>>(
|
const [eyeColor, setEyeColor] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(props.performer.eye_color, props.scraped.eye_color)
|
new ScrapeResult<string>(props.performer.eye_color, props.scraped.eye_color)
|
||||||
);
|
);
|
||||||
const [height, setHeight] = useState<ScrapeResult<string>>(
|
const [height, setHeight] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(props.performer.height, props.scraped.height)
|
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>>(
|
const [measurements, setMeasurements] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(
|
new ScrapeResult<string>(
|
||||||
props.performer.measurements,
|
props.performer.measurements,
|
||||||
|
|
@ -201,6 +219,9 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||||
translateScrapedGender(props.scraped.gender)
|
translateScrapedGender(props.scraped.gender)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
const [details, setDetails] = useState<ScrapeResult<string>>(
|
||||||
|
new ScrapeResult<string>(props.performer.details, props.scraped.details)
|
||||||
|
);
|
||||||
|
|
||||||
const [createTag] = useTagCreate({ name: "" });
|
const [createTag] = useTagCreate({ name: "" });
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
@ -281,6 +302,10 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||||
gender,
|
gender,
|
||||||
image,
|
image,
|
||||||
tags,
|
tags,
|
||||||
|
details,
|
||||||
|
deathDate,
|
||||||
|
hairColor,
|
||||||
|
weight,
|
||||||
];
|
];
|
||||||
// don't show the dialog if nothing was scraped
|
// don't show the dialog if nothing was scraped
|
||||||
if (allFields.every((r) => !r.scraped)) {
|
if (allFields.every((r) => !r.scraped)) {
|
||||||
|
|
@ -348,6 +373,10 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
image: image.getNewValue(),
|
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}
|
result={birthdate}
|
||||||
onChange={(value) => setBirthdate(value)}
|
onChange={(value) => setBirthdate(value)}
|
||||||
/>
|
/>
|
||||||
|
<ScrapedInputGroupRow
|
||||||
|
title="Death Date"
|
||||||
|
result={deathDate}
|
||||||
|
onChange={(value) => setDeathDate(value)}
|
||||||
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Ethnicity"
|
title="Ethnicity"
|
||||||
result={ethnicity}
|
result={ethnicity}
|
||||||
|
|
@ -380,11 +414,21 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||||
result={country}
|
result={country}
|
||||||
onChange={(value) => setCountry(value)}
|
onChange={(value) => setCountry(value)}
|
||||||
/>
|
/>
|
||||||
|
<ScrapedInputGroupRow
|
||||||
|
title="Hair Color"
|
||||||
|
result={hairColor}
|
||||||
|
onChange={(value) => setHairColor(value)}
|
||||||
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Eye Color"
|
title="Eye Color"
|
||||||
result={eyeColor}
|
result={eyeColor}
|
||||||
onChange={(value) => setEyeColor(value)}
|
onChange={(value) => setEyeColor(value)}
|
||||||
/>
|
/>
|
||||||
|
<ScrapedInputGroupRow
|
||||||
|
title="Weight"
|
||||||
|
result={weight}
|
||||||
|
onChange={(value) => setWeight(value)}
|
||||||
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Height"
|
title="Height"
|
||||||
result={height}
|
result={height}
|
||||||
|
|
@ -430,6 +474,11 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||||
result={instagram}
|
result={instagram}
|
||||||
onChange={(value) => setInstagram(value)}
|
onChange={(value) => setInstagram(value)}
|
||||||
/>
|
/>
|
||||||
|
<ScrapedTextAreaRow
|
||||||
|
title="Details"
|
||||||
|
result={details}
|
||||||
|
onChange={(value) => setDetails(value)}
|
||||||
|
/>
|
||||||
{renderScrapedTagsRow(
|
{renderScrapedTagsRow(
|
||||||
tags,
|
tags,
|
||||||
(value) => setTags(value),
|
(value) => setTags(value),
|
||||||
|
|
|
||||||
|
|
@ -336,9 +336,11 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
||||||
try {
|
try {
|
||||||
const result = await createStudio({
|
const result = await createStudio({
|
||||||
variables: {
|
variables: {
|
||||||
|
input: {
|
||||||
name: toCreate.name,
|
name: toCreate.name,
|
||||||
url: toCreate.url,
|
url: toCreate.url,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// set the new studio as the value
|
// set the new studio as the value
|
||||||
|
|
@ -362,7 +364,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
||||||
try {
|
try {
|
||||||
performerInput = Object.assign(performerInput, toCreate);
|
performerInput = Object.assign(performerInput, toCreate);
|
||||||
const result = await createPerformer({
|
const result = await createPerformer({
|
||||||
variables: performerInput,
|
variables: { input: performerInput },
|
||||||
});
|
});
|
||||||
|
|
||||||
// add the new performer to the new performers value
|
// 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 onCreate = async (name: string) => {
|
||||||
const result = await createPerformer({
|
const result = await createPerformer({
|
||||||
variables: { name },
|
variables: { input: { name } },
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
item: result.data!.performerCreate!,
|
item: result.data!.performerCreate!,
|
||||||
|
|
@ -415,7 +415,11 @@ export const StudioSelect: React.FC<
|
||||||
);
|
);
|
||||||
|
|
||||||
const onCreate = async (name: string) => {
|
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" };
|
return { item: result.data!.studioCreate!, message: "Created studio" };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ export const Studio: React.FC = () => {
|
||||||
const [name, setName] = useState<string>();
|
const [name, setName] = useState<string>();
|
||||||
const [url, setUrl] = useState<string>();
|
const [url, setUrl] = useState<string>();
|
||||||
const [parentStudioId, setParentStudioId] = useState<string>();
|
const [parentStudioId, setParentStudioId] = useState<string>();
|
||||||
|
const [details, setDetails] = useState<string>();
|
||||||
|
|
||||||
// Studio state
|
// Studio state
|
||||||
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
|
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
|
||||||
|
|
@ -63,6 +64,7 @@ export const Studio: React.FC = () => {
|
||||||
setName(state.name);
|
setName(state.name);
|
||||||
setUrl(state.url ?? undefined);
|
setUrl(state.url ?? undefined);
|
||||||
setParentStudioId(state?.parent_studio?.id ?? undefined);
|
setParentStudioId(state?.parent_studio?.id ?? undefined);
|
||||||
|
setDetails(state.details ?? undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStudioData(studioData: Partial<GQL.StudioDataFragment>) {
|
function updateStudioData(studioData: Partial<GQL.StudioDataFragment>) {
|
||||||
|
|
@ -117,6 +119,7 @@ export const Studio: React.FC = () => {
|
||||||
name,
|
name,
|
||||||
url,
|
url,
|
||||||
image,
|
image,
|
||||||
|
details,
|
||||||
parent_id: parentStudioId ?? null,
|
parent_id: parentStudioId ?? null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -301,6 +304,12 @@ export const Studio: React.FC = () => {
|
||||||
isEditing: !!isEditing,
|
isEditing: !!isEditing,
|
||||||
onChange: setUrl,
|
onChange: setUrl,
|
||||||
})}
|
})}
|
||||||
|
{TableUtils.renderTextArea({
|
||||||
|
title: "Details",
|
||||||
|
value: details,
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: setDetails,
|
||||||
|
})}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Parent Studio</td>
|
<td>Parent Studio</td>
|
||||||
<td>{renderStudio()}</td>
|
<td>{renderStudio()}</td>
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,13 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
||||||
text={performer.birthdate ?? "Unknown"}
|
text={performer.birthdate ?? "Unknown"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="row no-gutters">
|
||||||
<strong className="col-6">Ethnicity:</strong>
|
<strong className="col-6">Ethnicity:</strong>
|
||||||
<TruncatedText
|
<TruncatedText
|
||||||
|
|
@ -96,6 +103,13 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
||||||
<strong className="col-6">Country:</strong>
|
<strong className="col-6">Country:</strong>
|
||||||
<TruncatedText className="col-6" text={performer.country ?? ""} />
|
<TruncatedText className="col-6" text={performer.country ?? ""} />
|
||||||
</div>
|
</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">
|
<div className="row no-gutters">
|
||||||
<strong className="col-6">Eye Color:</strong>
|
<strong className="col-6">Eye Color:</strong>
|
||||||
<TruncatedText
|
<TruncatedText
|
||||||
|
|
@ -107,6 +121,10 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
||||||
<strong className="col-6">Height:</strong>
|
<strong className="col-6">Height:</strong>
|
||||||
<TruncatedText className="col-6" text={performer.height} />
|
<TruncatedText className="col-6" text={performer.height} />
|
||||||
</div>
|
</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">
|
<div className="row no-gutters">
|
||||||
<strong className="col-6">Measurements:</strong>
|
<strong className="col-6">Measurements:</strong>
|
||||||
<TruncatedText className="col-6" text={performer.measurements} />
|
<TruncatedText className="col-6" text={performer.measurements} />
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,10 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
||||||
stash_id: stashID,
|
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);
|
const res = await createPerformer(performerInput, stashID);
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export const useCreatePerformer = () => {
|
||||||
|
|
||||||
const handleCreate = (performer: GQL.PerformerCreateInput, stashID: string) =>
|
const handleCreate = (performer: GQL.PerformerCreateInput, stashID: string) =>
|
||||||
createPerformer({
|
createPerformer({
|
||||||
variables: performer,
|
variables: { input: performer },
|
||||||
update: (store, newPerformer) => {
|
update: (store, newPerformer) => {
|
||||||
if (!newPerformer?.data?.performerCreate) return;
|
if (!newPerformer?.data?.performerCreate) return;
|
||||||
|
|
||||||
|
|
@ -159,7 +159,7 @@ export const useCreateStudio = () => {
|
||||||
|
|
||||||
const handleCreate = (studio: GQL.StudioCreateInput, stashID: string) =>
|
const handleCreate = (studio: GQL.StudioCreateInput, stashID: string) =>
|
||||||
createStudio({
|
createStudio({
|
||||||
variables: studio,
|
variables: { input: studio },
|
||||||
update: (store, result) => {
|
update: (store, result) => {
|
||||||
if (!result?.data?.studioCreate) return;
|
if (!result?.data?.studioCreate) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,10 @@ export interface IStashBoxPerformer {
|
||||||
piercings?: string;
|
piercings?: string;
|
||||||
aliases?: string;
|
aliases?: string;
|
||||||
images: string[];
|
images: string[];
|
||||||
|
details?: string;
|
||||||
|
death_date?: string;
|
||||||
|
hair_color?: string;
|
||||||
|
weight?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IStashBoxTag {
|
export interface IStashBoxTag {
|
||||||
|
|
@ -126,6 +130,9 @@ const selectPerformers = (
|
||||||
piercings: p.piercings ? toTitleCase(p.piercings) : undefined,
|
piercings: p.piercings ? toTitleCase(p.piercings) : undefined,
|
||||||
aliases: p.aliases ?? undefined,
|
aliases: p.aliases ?? undefined,
|
||||||
images: p.images ?? [],
|
images: p.images ?? [],
|
||||||
|
details: p.details ?? undefined,
|
||||||
|
death_date: p.death_date ?? undefined,
|
||||||
|
hair_color: p.hair_color ?? undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const selectScenes = (
|
export const selectScenes = (
|
||||||
|
|
|
||||||
|
|
@ -584,7 +584,7 @@ export const studioMutationImpactedQueries = [
|
||||||
|
|
||||||
export const useStudioCreate = (input: GQL.StudioCreateInput) =>
|
export const useStudioCreate = (input: GQL.StudioCreateInput) =>
|
||||||
GQL.useStudioCreateMutation({
|
GQL.useStudioCreateMutation({
|
||||||
variables: input,
|
variables: { input },
|
||||||
refetchQueries: getQueryNames([GQL.AllStudiosForFilterDocument]),
|
refetchQueries: getQueryNames([GQL.AllStudiosForFilterDocument]),
|
||||||
update: deleteCache([
|
update: deleteCache([
|
||||||
GQL.FindStudiosDocument,
|
GQL.FindStudiosDocument,
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,13 @@ url
|
||||||
twitter
|
twitter
|
||||||
instagram
|
instagram
|
||||||
birthdate
|
birthdate
|
||||||
|
death_date
|
||||||
ethnicity
|
ethnicity
|
||||||
country
|
country
|
||||||
|
hair_color
|
||||||
eye_color
|
eye_color
|
||||||
height
|
height
|
||||||
|
weight
|
||||||
measurements
|
measurements
|
||||||
fake_tits
|
fake_tits
|
||||||
career_length
|
career_length
|
||||||
|
|
@ -64,6 +67,7 @@ piercings
|
||||||
image (base64 encoding of the image file)
|
image (base64 encoding of the image file)
|
||||||
created_at
|
created_at
|
||||||
updated_at
|
updated_at
|
||||||
|
details
|
||||||
```
|
```
|
||||||
|
|
||||||
## Studio
|
## Studio
|
||||||
|
|
@ -73,6 +77,7 @@ url
|
||||||
image (base64 encoding of the image file)
|
image (base64 encoding of the image file)
|
||||||
created_at
|
created_at
|
||||||
updated_at
|
updated_at
|
||||||
|
details
|
||||||
```
|
```
|
||||||
|
|
||||||
## Scene
|
## 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",
|
"description": "Birthdate of the performer. Format is YYYY-MM-DD",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"death_date": {
|
||||||
|
"description": "Death date of the performer. Format is YYYY-MM-DD",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"ethnicity": {
|
"ethnicity": {
|
||||||
"description": "Ethnicity of the Performer. Possible values are black, white, asian or hispanic",
|
"description": "Ethnicity of the Performer. Possible values are black, white, asian or hispanic",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -237,6 +246,10 @@ For those preferring the json-format, defined [here](https://json-schema.org/),
|
||||||
"description": "Country of the performer",
|
"description": "Country of the performer",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"hair_color": {
|
||||||
|
"description": "Hair color of the performer",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"eye_color": {
|
"eye_color": {
|
||||||
"description": "Eye color of the performer",
|
"description": "Eye color of the performer",
|
||||||
"type": "string"
|
"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",
|
"description": "Height of the performer in centimeters",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"weight": {
|
||||||
|
"description": "Weight of the performer in kilograms",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"measurements": {
|
"measurements": {
|
||||||
"description": "Measurements of the performer",
|
"description": "Measurements of the performer",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -276,6 +293,10 @@ For those preferring the json-format, defined [here](https://json-schema.org/),
|
||||||
"updated_at": {
|
"updated_at": {
|
||||||
"description": "The time this performers data was last changed in the database. Format is YYYY-MM-DDThh:mm:ssTZD",
|
"description": "The time this performers data was last changed in the database. Format is YYYY-MM-DDThh:mm:ssTZD",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"details": {
|
||||||
|
"description": "Description of the performer",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name", "ethnicity", "image", "created_at", "updated_at"]
|
"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": {
|
"updated_at": {
|
||||||
"description": "The time this studios data was last changed in the database. Format is YYYY-MM-DDThh:mm:ssTZD",
|
"description": "The time this studios data was last changed in the database. Format is YYYY-MM-DDThh:mm:ssTZD",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"details": {
|
||||||
|
"description": "Description of the studio",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name", "image", "created_at", "updated_at"]
|
"required": ["name", "image", "created_at", "updated_at"]
|
||||||
|
|
|
||||||
|
|
@ -740,10 +740,13 @@ URL
|
||||||
Twitter
|
Twitter
|
||||||
Instagram
|
Instagram
|
||||||
Birthdate
|
Birthdate
|
||||||
|
DeathDate
|
||||||
Ethnicity
|
Ethnicity
|
||||||
Country
|
Country
|
||||||
|
HairColor
|
||||||
EyeColor
|
EyeColor
|
||||||
Height
|
Height
|
||||||
|
Weight
|
||||||
Measurements
|
Measurements
|
||||||
FakeTits
|
FakeTits
|
||||||
CareerLength
|
CareerLength
|
||||||
|
|
@ -752,6 +755,7 @@ Piercings
|
||||||
Aliases
|
Aliases
|
||||||
Tags (see Tag fields)
|
Tags (see Tag fields)
|
||||||
Image
|
Image
|
||||||
|
Details
|
||||||
```
|
```
|
||||||
|
|
||||||
*Note:* - `Gender` must be one of `male`, `female`, `transgender_male`, `transgender_female`, `intersex`, `non_binary` (case insensitive).
|
*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"
|
| "age"
|
||||||
| "ethnicity"
|
| "ethnicity"
|
||||||
| "country"
|
| "country"
|
||||||
|
| "hair_color"
|
||||||
| "eye_color"
|
| "eye_color"
|
||||||
| "height"
|
| "height"
|
||||||
|
| "weight"
|
||||||
| "measurements"
|
| "measurements"
|
||||||
| "fake_tits"
|
| "fake_tits"
|
||||||
| "career_length"
|
| "career_length"
|
||||||
|
|
@ -49,6 +51,7 @@ export type CriterionType =
|
||||||
| "image_count"
|
| "image_count"
|
||||||
| "gallery_count"
|
| "gallery_count"
|
||||||
| "performer_count"
|
| "performer_count"
|
||||||
|
| "death_year"
|
||||||
| "url";
|
| "url";
|
||||||
|
|
||||||
type Option = string | number | IOptionType;
|
type Option = string | number | IOptionType;
|
||||||
|
|
@ -103,16 +106,22 @@ export abstract class Criterion {
|
||||||
return "Galleries";
|
return "Galleries";
|
||||||
case "birth_year":
|
case "birth_year":
|
||||||
return "Birth Year";
|
return "Birth Year";
|
||||||
|
case "death_year":
|
||||||
|
return "Death Year";
|
||||||
case "age":
|
case "age":
|
||||||
return "Age";
|
return "Age";
|
||||||
case "ethnicity":
|
case "ethnicity":
|
||||||
return "Ethnicity";
|
return "Ethnicity";
|
||||||
case "country":
|
case "country":
|
||||||
return "Country";
|
return "Country";
|
||||||
|
case "hair_color":
|
||||||
|
return "Hair Color";
|
||||||
case "eye_color":
|
case "eye_color":
|
||||||
return "Eye Color";
|
return "Eye Color";
|
||||||
case "height":
|
case "height":
|
||||||
return "Height";
|
return "Height";
|
||||||
|
case "weight":
|
||||||
|
return "Weight";
|
||||||
case "measurements":
|
case "measurements":
|
||||||
return "Measurements";
|
return "Measurements";
|
||||||
case "fake_tits":
|
case "fake_tits":
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,10 @@ export class PerformerIsMissingCriterion extends IsMissingCriterion {
|
||||||
"instagram",
|
"instagram",
|
||||||
"ethnicity",
|
"ethnicity",
|
||||||
"country",
|
"country",
|
||||||
|
"hair_color",
|
||||||
"eye_color",
|
"eye_color",
|
||||||
"height",
|
"height",
|
||||||
|
"weight",
|
||||||
"measurements",
|
"measurements",
|
||||||
"fake_tits",
|
"fake_tits",
|
||||||
"career_length",
|
"career_length",
|
||||||
|
|
@ -65,6 +67,7 @@ export class PerformerIsMissingCriterion extends IsMissingCriterion {
|
||||||
"scenes",
|
"scenes",
|
||||||
"image",
|
"image",
|
||||||
"stash_id",
|
"stash_id",
|
||||||
|
"details",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,7 +107,7 @@ export class TagIsMissingCriterionOption implements ICriterionOption {
|
||||||
|
|
||||||
export class StudioIsMissingCriterion extends IsMissingCriterion {
|
export class StudioIsMissingCriterion extends IsMissingCriterion {
|
||||||
public type: CriterionType = "studioIsMissing";
|
public type: CriterionType = "studioIsMissing";
|
||||||
public options: string[] = ["image", "stash_id"];
|
public options: string[] = ["image", "stash_id", "details"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StudioIsMissingCriterionOption implements ICriterionOption {
|
export class StudioIsMissingCriterionOption implements ICriterionOption {
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,8 @@ export function makeCriteria(type: CriterionType = "none") {
|
||||||
case "galleries":
|
case "galleries":
|
||||||
return new GalleriesCriterion();
|
return new GalleriesCriterion();
|
||||||
case "birth_year":
|
case "birth_year":
|
||||||
|
case "death_year":
|
||||||
|
case "weight":
|
||||||
return new NumberCriterion(type, type);
|
return new NumberCriterion(type, type);
|
||||||
case "age":
|
case "age":
|
||||||
return new MandatoryNumberCriterion(type, type);
|
return new MandatoryNumberCriterion(type, type);
|
||||||
|
|
@ -95,6 +97,7 @@ export function makeCriteria(type: CriterionType = "none") {
|
||||||
return new GenderCriterion();
|
return new GenderCriterion();
|
||||||
case "ethnicity":
|
case "ethnicity":
|
||||||
case "country":
|
case "country":
|
||||||
|
case "hair_color":
|
||||||
case "eye_color":
|
case "eye_color":
|
||||||
case "height":
|
case "height":
|
||||||
case "measurements":
|
case "measurements":
|
||||||
|
|
|
||||||
|
|
@ -200,12 +200,18 @@ export class ListFilterModel {
|
||||||
];
|
];
|
||||||
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
|
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
|
||||||
|
|
||||||
const numberCriteria: CriterionType[] = ["birth_year", "age"];
|
const numberCriteria: CriterionType[] = [
|
||||||
|
"birth_year",
|
||||||
|
"death_year",
|
||||||
|
"age",
|
||||||
|
];
|
||||||
const stringCriteria: CriterionType[] = [
|
const stringCriteria: CriterionType[] = [
|
||||||
"ethnicity",
|
"ethnicity",
|
||||||
"country",
|
"country",
|
||||||
|
"hair_color",
|
||||||
"eye_color",
|
"eye_color",
|
||||||
"height",
|
"height",
|
||||||
|
"weight",
|
||||||
"measurements",
|
"measurements",
|
||||||
"fake_tits",
|
"fake_tits",
|
||||||
"career_length",
|
"career_length",
|
||||||
|
|
@ -650,6 +656,14 @@ export class ListFilterModel {
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "death_year": {
|
||||||
|
const dyCrit = criterion as NumberCriterion;
|
||||||
|
result.death_year = {
|
||||||
|
value: dyCrit.value,
|
||||||
|
modifier: dyCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "age": {
|
case "age": {
|
||||||
const ageCrit = criterion as NumberCriterion;
|
const ageCrit = criterion as NumberCriterion;
|
||||||
result.age = { value: ageCrit.value, modifier: ageCrit.modifier };
|
result.age = { value: ageCrit.value, modifier: ageCrit.modifier };
|
||||||
|
|
@ -671,6 +685,14 @@ export class ListFilterModel {
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "hair_color": {
|
||||||
|
const hcCrit = criterion as StringCriterion;
|
||||||
|
result.hair_color = {
|
||||||
|
value: hcCrit.value,
|
||||||
|
modifier: hcCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "eye_color": {
|
case "eye_color": {
|
||||||
const ecCrit = criterion as StringCriterion;
|
const ecCrit = criterion as StringCriterion;
|
||||||
result.eye_color = { value: ecCrit.value, modifier: ecCrit.modifier };
|
result.eye_color = { value: ecCrit.value, modifier: ecCrit.modifier };
|
||||||
|
|
@ -681,6 +703,11 @@ export class ListFilterModel {
|
||||||
result.height = { value: hCrit.value, modifier: hCrit.modifier };
|
result.height = { value: hCrit.value, modifier: hCrit.modifier };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "weight": {
|
||||||
|
const wCrit = criterion as StringCriterion;
|
||||||
|
result.weight = { value: wCrit.value, modifier: wCrit.modifier };
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "measurements": {
|
case "measurements": {
|
||||||
const mCrit = criterion as StringCriterion;
|
const mCrit = criterion as StringCriterion;
|
||||||
result.measurements = {
|
result.measurements = {
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ const stringToDate = (dateString: string) => {
|
||||||
return new Date(year, monthIndex, day, 0, 0, 0, 0);
|
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;
|
if (!dateString) return 0;
|
||||||
|
|
||||||
const birthdate = stringToDate(dateString);
|
const birthdate = stringToDate(dateString);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue