Added rating to performers and studios (#1308)

This commit is contained in:
julien0221
2021-04-26 04:48:32 +01:00
committed by GitHub
parent eefc628cf0
commit 70b66d91a0
39 changed files with 438 additions and 25 deletions

View File

@@ -12,4 +12,5 @@ fragment SlimPerformerData on Performer {
endpoint endpoint
stash_id stash_id
} }
rating
} }

View File

@@ -31,6 +31,7 @@ fragment PerformerData on Performer {
stash_id stash_id
endpoint endpoint
} }
rating
details details
death_date death_date
hair_color hair_color

View File

@@ -10,4 +10,5 @@ fragment SlimStudioData on Studio {
id id
} }
details details
rating
} }

View File

@@ -32,4 +32,5 @@ fragment StudioData on Studio {
endpoint endpoint
} }
details details
rating
} }

View File

@@ -1,14 +1,10 @@
mutation StudioCreate( mutation StudioCreate($input: StudioCreateInput!) {
$input: StudioCreateInput!) {
studioCreate(input: $input) { studioCreate(input: $input) {
...StudioData ...StudioData
} }
} }
mutation StudioUpdate( mutation StudioUpdate($input: StudioUpdateInput!) {
$input: StudioUpdateInput!) {
studioUpdate(input: $input) { studioUpdate(input: $input) {
...StudioData ...StudioData
} }

View File

@@ -71,6 +71,8 @@ input PerformerFilterType {
gallery_count: IntCriterionInput gallery_count: IntCriterionInput
"""Filter by StashID""" """Filter by StashID"""
stash_id: String stash_id: String
"""Filter by rating"""
rating: IntCriterionInput
"""Filter by url""" """Filter by url"""
url: StringCriterionInput url: StringCriterionInput
"""Filter by hair color""" """Filter by hair color"""
@@ -149,6 +151,8 @@ input StudioFilterType {
stash_id: String stash_id: String
"""Filter to only include studios missing this property""" """Filter to only include studios missing this property"""
is_missing: String is_missing: String
"""Filter by rating"""
rating: IntCriterionInput
"""Filter by scene count""" """Filter by scene count"""
scene_count: IntCriterionInput scene_count: IntCriterionInput
"""Filter by image count""" """Filter by image count"""

View File

@@ -35,6 +35,7 @@ type Performer {
gallery_count: Int # Resolver gallery_count: Int # Resolver
scenes: [Scene!]! scenes: [Scene!]!
stash_ids: [StashID!]! stash_ids: [StashID!]!
rating: Int
details: String details: String
death_date: String death_date: String
hair_color: String hair_color: String
@@ -63,6 +64,7 @@ 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!]
rating: Int
details: String details: String
death_date: String death_date: String
hair_color: String hair_color: String
@@ -92,6 +94,7 @@ 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!]
rating: Int
details: String details: String
death_date: String death_date: String
hair_color: String hair_color: String
@@ -118,6 +121,7 @@ input BulkPerformerUpdateInput {
instagram: String instagram: String
favorite: Boolean favorite: Boolean
tag_ids: BulkUpdateIds tag_ids: BulkUpdateIds
rating: Int
details: String details: String
death_date: String death_date: String
hair_color: String hair_color: String

View File

@@ -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!]!
rating: Int
details: String details: String
} }
@@ -21,6 +22,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!]
rating: Int
details: String details: String
} }
@@ -32,6 +34,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!]
rating: Int
details: String details: String
} }

View File

@@ -209,6 +209,14 @@ func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer)
return ret, nil return ret, nil
} }
func (r *performerResolver) Rating(ctx context.Context, obj *models.Performer) (*int, error) {
if obj.Rating.Valid {
rating := int(obj.Rating.Int64)
return &rating, nil
}
return nil, nil
}
func (r *performerResolver) Details(ctx context.Context, obj *models.Performer) (*string, error) { func (r *performerResolver) Details(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Details.Valid { if obj.Details.Valid {
return &obj.Details.String, nil return &obj.Details.String, nil

View File

@@ -117,6 +117,14 @@ func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) (ret
return ret, nil return ret, nil
} }
func (r *studioResolver) Rating(ctx context.Context, obj *models.Studio) (*int, error) {
if obj.Rating.Valid {
rating := int(obj.Rating.Int64)
return &rating, nil
}
return nil, nil
}
func (r *studioResolver) Details(ctx context.Context, obj *models.Studio) (*string, error) { func (r *studioResolver) Details(ctx context.Context, obj *models.Studio) (*string, error) {
if obj.Details.Valid { if obj.Details.Valid {
return &obj.Details.String, nil return &obj.Details.String, nil

View File

@@ -85,6 +85,11 @@ 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.Rating != nil {
newPerformer.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
newPerformer.Rating = sql.NullInt64{Valid: false}
}
if input.Details != nil { if input.Details != nil {
newPerformer.Details = sql.NullString{String: *input.Details, Valid: true} newPerformer.Details = sql.NullString{String: *input.Details, Valid: true}
} }
@@ -198,6 +203,7 @@ 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.Rating = translator.nullInt64(input.Rating, "rating")
updatedPerformer.Details = translator.nullString(input.Details, "details") updatedPerformer.Details = translator.nullString(input.Details, "details")
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date") updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color") updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
@@ -304,6 +310,7 @@ 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.Rating = translator.nullInt64(input.Rating, "rating")
updatedPerformer.Details = translator.nullString(input.Details, "details") updatedPerformer.Details = translator.nullString(input.Details, "details")
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date") updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color") updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")

View File

@@ -42,6 +42,11 @@ 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.Rating != nil {
newStudio.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
newStudio.Rating = sql.NullInt64{Valid: false}
}
if input.Details != nil { if input.Details != nil {
newStudio.Details = sql.NullString{String: *input.Details, Valid: true} newStudio.Details = sql.NullString{String: *input.Details, Valid: true}
} }
@@ -115,6 +120,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.Details = translator.nullString(input.Details, "details")
updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id") updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id")
updatedStudio.Rating = translator.nullInt64(input.Rating, "rating")
// Start the transaction and save the studio // Start the transaction and save the studio
var studio *models.Studio var studio *models.Studio

View File

@@ -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 = 21 var appSchemaVersion uint = 22
var databaseSchemaVersion uint var databaseSchemaVersion uint
var ( var (

View File

@@ -0,0 +1,2 @@
ALTER TABLE `performers` ADD COLUMN `rating` tinyint;
ALTER TABLE `studios` ADD COLUMN `rating` tinyint;

View File

@@ -30,6 +30,7 @@ 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"`
Rating int `json:"rating,omitempty"`
Details string `json:"details,omitempty"` Details string `json:"details,omitempty"`
DeathDate string `json:"death_date,omitempty"` DeathDate string `json:"death_date,omitempty"`
HairColor string `json:"hair_color,omitempty"` HairColor string `json:"hair_color,omitempty"`

View File

@@ -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"`
Rating int `json:"rating,omitempty"`
Details string `json:"details,omitempty"` Details string `json:"details,omitempty"`
} }

View File

@@ -29,6 +29,7 @@ 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"`
Rating sql.NullInt64 `db:"rating" json:"rating"`
Details sql.NullString `db:"details" json:"details"` Details sql.NullString `db:"details" json:"details"`
DeathDate SQLiteDate `db:"death_date" json:"death_date"` DeathDate SQLiteDate `db:"death_date" json:"death_date"`
HairColor sql.NullString `db:"hair_color" json:"hair_color"` HairColor sql.NullString `db:"hair_color" json:"hair_color"`
@@ -57,6 +58,7 @@ 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"`
Rating *sql.NullInt64 `db:"rating" json:"rating"`
Details *sql.NullString `db:"details" json:"details"` Details *sql.NullString `db:"details" json:"details"`
DeathDate *SQLiteDate `db:"death_date" json:"death_date"` DeathDate *SQLiteDate `db:"death_date" json:"death_date"`
HairColor *sql.NullString `db:"hair_color" json:"hair_color"` HairColor *sql.NullString `db:"hair_color" json:"hair_color"`

View File

@@ -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"`
Rating sql.NullInt64 `db:"rating" json:"rating"`
Details sql.NullString `db:"details" json:"details"` Details sql.NullString `db:"details" json:"details"`
} }
@@ -26,6 +27,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"`
Rating *sql.NullInt64 `db:"rating" json:"rating"`
Details *sql.NullString `db:"details" json:"details"` Details *sql.NullString `db:"details" json:"details"`
} }

View File

@@ -66,6 +66,9 @@ 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.Rating.Valid {
newPerformerJSON.Rating = int(performer.Rating.Int64)
}
if performer.Details.Valid { if performer.Details.Valid {
newPerformerJSON.Details = performer.Details.String newPerformerJSON.Details = performer.Details.String
} }

View File

@@ -36,6 +36,7 @@ const (
piercings = "piercings" piercings = "piercings"
tattoos = "tattoos" tattoos = "tattoos"
twitter = "twitter" twitter = "twitter"
rating = 5
details = "details" details = "details"
hairColor = "hairColor" hairColor = "hairColor"
weight = 60 weight = 60
@@ -86,6 +87,7 @@ func createFullPerformer(id int, name string) *models.Performer {
UpdatedAt: models.SQLiteTimestamp{ UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime, Timestamp: updateTime,
}, },
Rating: models.NullInt64(rating),
Details: models.NullString(details), Details: models.NullString(details),
DeathDate: deathDate, DeathDate: deathDate,
HairColor: models.NullString(hairColor), HairColor: models.NullString(hairColor),
@@ -133,6 +135,7 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
UpdatedAt: models.JSONTime{ UpdatedAt: models.JSONTime{
Time: updateTime, Time: updateTime,
}, },
Rating: rating,
Image: image, Image: image,
Details: details, Details: details,
DeathDate: deathDate.String, DeathDate: deathDate.String,

View File

@@ -224,6 +224,9 @@ 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.Rating != 0 {
newPerformer.Rating = sql.NullInt64{Int64: int64(performerJSON.Rating), Valid: true}
}
if performerJSON.Details != "" { if performerJSON.Details != "" {
newPerformer.Details = sql.NullString{String: performerJSON.Details, Valid: true} newPerformer.Details = sql.NullString{String: performerJSON.Details, Valid: true}
} }

View File

@@ -279,6 +279,7 @@ 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.handleIntCriterionInput(performerFilter.Rating, tableName+".rating")
query.handleStringCriterionInput(performerFilter.HairColor, tableName+".hair_color") query.handleStringCriterionInput(performerFilter.HairColor, tableName+".hair_color")
query.handleStringCriterionInput(performerFilter.URL, tableName+".url") query.handleStringCriterionInput(performerFilter.URL, tableName+".url")
query.handleIntCriterionInput(performerFilter.Weight, tableName+".weight") query.handleIntCriterionInput(performerFilter.Weight, tableName+".weight")

View File

@@ -617,6 +617,67 @@ func TestPerformerStashIDs(t *testing.T) {
t.Error(err.Error()) t.Error(err.Error())
} }
} }
func TestPerformerQueryRating(t *testing.T) {
const rating = 3
ratingCriterion := models.IntCriterionInput{
Value: rating,
Modifier: models.CriterionModifierEquals,
}
verifyPerformersRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierNotEquals
verifyPerformersRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
verifyPerformersRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierLessThan
verifyPerformersRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierIsNull
verifyPerformersRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierNotNull
verifyPerformersRating(t, ratingCriterion)
}
func verifyPerformersRating(t *testing.T, ratingCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Performer()
performerFilter := models.PerformerFilterType{
Rating: &ratingCriterion,
}
performers := queryPerformers(t, sqb, &performerFilter, nil)
for _, performer := range performers {
verifyInt64(t, performer.Rating, ratingCriterion)
}
return nil
})
}
func TestPerformerQueryIsMissingRating(t *testing.T) {
withTxn(func(r models.Repository) error {
sqb := r.Performer()
isMissing := "rating"
performerFilter := models.PerformerFilterType{
IsMissing: &isMissing,
}
performers := queryPerformers(t, sqb, &performerFilter, nil)
assert.True(t, len(performers) > 0)
for _, performer := range performers {
assert.True(t, !performer.Rating.Valid)
}
return nil
})
}
// TODO Update // TODO Update
// TODO Destroy // TODO Destroy

View File

@@ -183,10 +183,12 @@ func (qb *studioQueryBuilder) Query(studioFilter *models.StudioFilterType, findF
query.addArg(stashIDFilter) query.addArg(stashIDFilter)
} }
if rating := studioFilter.Rating; rating != nil {
query.handleIntCriterionInput(studioFilter.Rating, "studios.rating")
}
query.handleCountCriterion(studioFilter.SceneCount, studioTable, sceneTable, studioIDColumn) query.handleCountCriterion(studioFilter.SceneCount, studioTable, sceneTable, studioIDColumn)
query.handleCountCriterion(studioFilter.ImageCount, studioTable, imageTable, studioIDColumn) query.handleCountCriterion(studioFilter.ImageCount, studioTable, imageTable, studioIDColumn)
query.handleCountCriterion(studioFilter.GalleryCount, studioTable, galleryTable, studioIDColumn) query.handleCountCriterion(studioFilter.GalleryCount, studioTable, galleryTable, studioIDColumn)
query.handleStringCriterionInput(studioFilter.URL, "studios.url") query.handleStringCriterionInput(studioFilter.URL, "studios.url")
if isMissingFilter := studioFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" { if isMissingFilter := studioFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {

View File

@@ -482,17 +482,42 @@ func TestStudioQueryURL(t *testing.T) {
verifyStudioQuery(t, filter, verifyFn) verifyStudioQuery(t, filter, verifyFn)
} }
func TestStudioQueryRating(t *testing.T) {
const rating = 3
ratingCriterion := models.IntCriterionInput{
Value: rating,
Modifier: models.CriterionModifierEquals,
}
verifyStudiosRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierNotEquals
verifyStudiosRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
verifyStudiosRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierLessThan
verifyStudiosRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierIsNull
verifyStudiosRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierNotNull
verifyStudiosRating(t, ratingCriterion)
}
func verifyStudioQuery(t *testing.T, filter models.StudioFilterType, verifyFn func(s *models.Studio)) { func verifyStudioQuery(t *testing.T, filter models.StudioFilterType, verifyFn func(s *models.Studio)) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
t.Helper() t.Helper()
sqb := r.Studio() sqb := r.Studio()
galleries := queryStudio(t, sqb, &filter, nil) studios := queryStudio(t, sqb, &filter, nil)
// assume it should find at least one // assume it should find at least one
assert.Greater(t, len(galleries), 0) assert.Greater(t, len(studios), 0)
for _, studio := range galleries { for _, studio := range studios {
verifyFn(studio) verifyFn(studio)
} }
@@ -500,6 +525,51 @@ func verifyStudioQuery(t *testing.T, filter models.StudioFilterType, verifyFn fu
}) })
} }
func verifyStudiosRating(t *testing.T, ratingCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Studio()
studioFilter := models.StudioFilterType{
Rating: &ratingCriterion,
}
studios, _, err := sqb.Query(&studioFilter, nil)
if err != nil {
t.Errorf("Error querying studio: %s", err.Error())
}
for _, studio := range studios {
verifyInt64(t, studio.Rating, ratingCriterion)
}
return nil
})
}
func TestStudioQueryIsMissingRating(t *testing.T) {
withTxn(func(r models.Repository) error {
sqb := r.Studio()
isMissing := "rating"
studioFilter := models.StudioFilterType{
IsMissing: &isMissing,
}
studios, _, err := sqb.Query(&studioFilter, nil)
if err != nil {
t.Errorf("Error querying studio: %s", err.Error())
}
assert.True(t, len(studios) > 0)
for _, studio := range studios {
assert.True(t, !studio.Rating.Valid)
}
return nil
})
}
func queryStudio(t *testing.T, sqb models.StudioReader, studioFilter *models.StudioFilterType, findFilter *models.FindFilterType) []*models.Studio { func queryStudio(t *testing.T, sqb models.StudioReader, studioFilter *models.StudioFilterType, findFilter *models.FindFilterType) []*models.Studio {
studios, _, err := sqb.Query(studioFilter, findFilter) studios, _, err := sqb.Query(studioFilter, findFilter)
if err != nil { if err != nil {

View File

@@ -38,6 +38,10 @@ func ToJSON(reader models.StudioReader, studio *models.Studio) (*jsonschema.Stud
} }
} }
if studio.Rating.Valid {
newStudioJSON.Rating = int(studio.Rating.Int64)
}
image, err := reader.GetImage(studio.ID) image, err := reader.GetImage(studio.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting studio image: %s", err.Error()) return nil, fmt.Errorf("error getting studio image: %s", err.Error())

View File

@@ -25,10 +25,10 @@ const (
) )
const ( const (
studioName = "testStudio" studioName = "testStudio"
url = "url" url = "url"
details = "details" details = "details"
rating = 5
parentStudioName = "parentStudio" parentStudioName = "parentStudio"
) )
@@ -55,6 +55,7 @@ func createFullStudio(id int, parentID int) models.Studio {
UpdatedAt: models.SQLiteTimestamp{ UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime, Timestamp: updateTime,
}, },
Rating: models.NullInt64(rating),
} }
if parentID != 0 { if parentID != 0 {
@@ -89,6 +90,7 @@ func createFullJSONStudio(parentStudio, image string) *jsonschema.Studio {
}, },
ParentStudio: parentStudio, ParentStudio: parentStudio,
Image: image, Image: image,
Rating: rating,
} }
} }

View File

@@ -31,6 +31,7 @@ func (i *Importer) PreImport() error {
Details: sql.NullString{String: i.Input.Details, 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()},
Rating: sql.NullInt64{Int64: int64(i.Input.Rating), Valid: true},
} }
if err := i.populateParentStudio(); err != nil { if err := i.populateParentStudio(); err != nil {

View File

@@ -1,4 +1,5 @@
### ✨ New Features ### ✨ New Features
* Added rating field to performers and studios.
* Support serving UI from specific directory location. * Support serving UI from specific directory location.
* Added details, death date, hair color, and weight to Performers. * Added details, death date, hair color, and weight to Performers.
* Added `lbToKg` post-process action for performer scrapers. * Added `lbToKg` post-process action for performer scrapers.

View File

@@ -1,11 +1,13 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Form } from "react-bootstrap"; import { Form, Col, Row } from "react-bootstrap";
import _ from "lodash"; import _ from "lodash";
import { useBulkPerformerUpdate } from "src/core/StashService"; import { useBulkPerformerUpdate } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { Modal } from "src/components/Shared"; import { Modal } from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { FormUtils } from "src/utils";
import MultiSet from "../Shared/MultiSet"; import MultiSet from "../Shared/MultiSet";
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
interface IListOperationProps { interface IListOperationProps {
selected: GQL.SlimPerformerDataFragment[]; selected: GQL.SlimPerformerDataFragment[];
@@ -16,7 +18,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
props: IListOperationProps props: IListOperationProps
) => { ) => {
const Toast = useToast(); const Toast = useToast();
const [rating, setRating] = useState<number>();
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>( const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
GQL.BulkUpdateIdMode.Add GQL.BulkUpdateIdMode.Add
); );
@@ -43,6 +45,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
function getPerformerInput(): GQL.BulkPerformerUpdateInput { function getPerformerInput(): GQL.BulkPerformerUpdateInput {
// need to determine what we are actually setting on each performer // need to determine what we are actually setting on each performer
const aggregateTagIds = getTagIds(props.selected); const aggregateTagIds = getTagIds(props.selected);
const aggregateRating = getRating(props.selected);
const performerInput: GQL.BulkPerformerUpdateInput = { const performerInput: GQL.BulkPerformerUpdateInput = {
ids: props.selected.map((performer) => { ids: props.selected.map((performer) => {
@@ -50,6 +53,19 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
}), }),
}; };
// if rating is undefined
if (rating === undefined) {
// and all galleries have the same rating, then we are unsetting the rating.
if (aggregateRating) {
// null to unset rating
performerInput.rating = null;
}
// otherwise not setting the rating
} else {
// if rating is set, then we are setting the rating for all
performerInput.rating = rating;
}
// if tagIds non-empty, then we are setting them // if tagIds non-empty, then we are setting them
if ( if (
tagMode === GQL.BulkUpdateIdMode.Set && tagMode === GQL.BulkUpdateIdMode.Set &&
@@ -106,19 +122,38 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
return ret; return ret;
} }
function getRating(state: GQL.SlimPerformerDataFragment[]) {
let ret: number | undefined;
let first = true;
state.forEach((performer) => {
if (first) {
ret = performer.rating ?? undefined;
first = false;
} else if (ret !== performer.rating) {
ret = undefined;
}
});
return ret;
}
useEffect(() => { useEffect(() => {
const state = props.selected; const state = props.selected;
let updateTagIds: string[] = []; let updateTagIds: string[] = [];
let updateFavorite: boolean | undefined; let updateFavorite: boolean | undefined;
let updateRating: number | undefined;
let first = true; let first = true;
state.forEach((performer: GQL.SlimPerformerDataFragment) => { state.forEach((performer: GQL.SlimPerformerDataFragment) => {
const performerTagIDs = (performer.tags ?? []).map((p) => p.id).sort(); const performerTagIDs = (performer.tags ?? []).map((p) => p.id).sort();
const performerRating = performer.rating;
if (first) { if (first) {
updateTagIds = performerTagIDs; updateTagIds = performerTagIDs;
first = false; first = false;
updateFavorite = performer.favorite; updateFavorite = performer.favorite;
updateRating = performerRating ?? undefined;
} else { } else {
if (!_.isEqual(performerTagIDs, updateTagIds)) { if (!_.isEqual(performerTagIDs, updateTagIds)) {
updateTagIds = []; updateTagIds = [];
@@ -126,6 +161,9 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
if (performer.favorite !== updateFavorite) { if (performer.favorite !== updateFavorite) {
updateFavorite = undefined; updateFavorite = undefined;
} }
if (performerRating !== updateRating) {
updateRating = undefined;
}
} }
}); });
@@ -133,6 +171,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
setTagIds(updateTagIds); setTagIds(updateTagIds);
} }
setFavorite(updateFavorite); setFavorite(updateFavorite);
setRating(updateRating);
}, [props.selected, tagMode]); }, [props.selected, tagMode]);
useEffect(() => { useEffect(() => {
@@ -201,6 +240,18 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
}} }}
isRunning={isUpdating} isRunning={isUpdating}
> >
<Form.Group controlId="rating" as={Row}>
{FormUtils.renderLabel({
title: "Rating",
})}
<Col xs={9}>
<RatingStars
value={rating}
onSetRating={(value) => setRating(value)}
disabled={isUpdating}
/>
</Col>
</Form.Group>
<Form> <Form>
<Form.Group controlId="tags"> <Form.Group controlId="tags">
<Form.Label>Tags</Form.Label> <Form.Label>Tags</Form.Label>

View File

@@ -1,6 +1,5 @@
import React from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { NavUtils, TextUtils } from "src/utils"; import { NavUtils, TextUtils } from "src/utils";
import { import {
@@ -35,13 +34,13 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
); );
const ageString = `${age} years old${ageFromDate ? " in this scene." : "."}`; const ageString = `${age} years old${ageFromDate ? " in this scene." : "."}`;
function maybeRenderFavoriteBanner() { function maybeRenderFavoriteIcon() {
if (performer.favorite === false) { if (performer.favorite === false) {
return; return;
} }
return ( return (
<div className="rating-banner rating-5"> <div className="favorite">
<FormattedMessage id="favourite" defaultMessage="Favourite" /> <Icon icon="heart" size="2x" />
</div> </div>
); );
} }
@@ -120,6 +119,21 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
} }
} }
function maybeRenderRatingBanner() {
if (!performer.rating) {
return;
}
return (
<div
className={`rating-banner ${
performer.rating ? `rating-${performer.rating}` : ""
}`}
>
RATING: {performer.rating}
</div>
);
}
return ( return (
<BasicCard <BasicCard
className="performer-card" className="performer-card"
@@ -131,7 +145,8 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
alt={performer.name ?? ""} alt={performer.name ?? ""}
src={performer.image_path ?? ""} src={performer.image_path ?? ""}
/> />
{maybeRenderFavoriteBanner()} {maybeRenderFavoriteIcon()}
{maybeRenderRatingBanner()}
</> </>
} }
details={ details={

View File

@@ -5,6 +5,7 @@ import * as GQL from "src/core/generated-graphql";
import { genderToString } from "src/core/StashService"; import { genderToString } from "src/core/StashService";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
import { TextField, URLField } from "src/utils/field"; import { TextField, URLField } from "src/utils/field";
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
interface IPerformerDetails { interface IPerformerDetails {
performer: Partial<GQL.PerformerDataFragment>; performer: Partial<GQL.PerformerDataFragment>;
@@ -35,6 +36,21 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
); );
} }
function renderRating() {
if (!performer.rating) {
return null;
}
return (
<dl className="row mb-0">
<dt className="col-3 col-xl-2">Rating:</dt>
<dd className="col-9 col-xl-10">
<RatingStars value={performer.rating} />
</dd>
</dl>
);
}
function renderStashIDs() { function renderStashIDs() {
if (!performer.stash_ids?.length) { if (!performer.stash_ids?.length) {
return; return;
@@ -139,6 +155,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
TextUtils.instagramURL TextUtils.instagramURL
)} )}
/> />
{renderRating()}
{renderTagsField()} {renderTagsField()}
{renderStashIDs()} {renderStashIDs()}
</> </>

View File

@@ -36,6 +36,7 @@ import { ImageUtils } from "src/utils";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { Prompt, useHistory } from "react-router-dom"; import { Prompt, useHistory } from "react-router-dom";
import { useFormik } from "formik"; import { useFormik } from "formik";
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
import { PerformerScrapeDialog } from "./PerformerScrapeDialog"; import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
import PerformerScrapeModal from "./PerformerScrapeModal"; import PerformerScrapeModal from "./PerformerScrapeModal";
@@ -62,7 +63,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
// Editing state // Editing state
const [scraper, setScraper] = useState<GQL.Scraper | undefined>(); const [scraper, setScraper] = useState<GQL.Scraper | undefined>();
const [newTags, setNewTags] = useState<GQL.ScrapedSceneTag[]>(); const [newTags, setNewTags] = useState<GQL.ScrapedSceneTag[]>();
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
// Network state // Network state
@@ -109,6 +109,7 @@ 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(),
rating: yup.number().optional().nullable(),
details: yup.string().optional(), details: yup.string().optional(),
death_date: yup.string().optional(), death_date: yup.string().optional(),
hair_color: yup.string().optional(), hair_color: yup.string().optional(),
@@ -135,6 +136,7 @@ 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,
rating: performer.rating ?? undefined,
details: performer.details ?? "", details: performer.details ?? "",
death_date: performer.death_date ?? "", death_date: performer.death_date ?? "",
hair_color: performer.hair_color ?? "", hair_color: performer.hair_color ?? "",
@@ -149,6 +151,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
onSubmit: (values) => onSave(values), onSubmit: (values) => onSave(values),
}); });
function setRating(v: number) {
formik.setFieldValue("rating", v);
}
function translateScrapedGender(scrapedGender?: string) { function translateScrapedGender(scrapedGender?: string) {
if (!scrapedGender) { if (!scrapedGender) {
return; return;
@@ -386,6 +392,30 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
}); });
} }
// numeric keypresses get caught by jwplayer, so blur the element
// if the rating sequence is started
Mousetrap.bind("r", () => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
Mousetrap.bind("0", () => setRating(NaN));
Mousetrap.bind("1", () => setRating(1));
Mousetrap.bind("2", () => setRating(2));
Mousetrap.bind("3", () => setRating(3));
Mousetrap.bind("4", () => setRating(4));
Mousetrap.bind("5", () => setRating(5));
setTimeout(() => {
Mousetrap.unbind("0");
Mousetrap.unbind("1");
Mousetrap.unbind("2");
Mousetrap.unbind("3");
Mousetrap.unbind("4");
Mousetrap.unbind("5");
}, 1000);
});
return () => { return () => {
Mousetrap.unbind("s s"); Mousetrap.unbind("s s");
@@ -424,6 +454,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
return { return {
...values, ...values,
gender: stringToGender(values.gender), gender: stringToGender(values.gender),
rating: values.rating ?? null,
weight: Number(values.weight), weight: Number(values.weight),
id: performer.id ?? "", id: performer.id ?? "",
}; };
@@ -909,6 +940,18 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
</Col> </Col>
</Form.Group> </Form.Group>
{renderTagsField()} {renderTagsField()}
<Form.Group controlId="rating" as={Row}>
<Form.Label column xs={labelXS} xl={labelXL}>
Rating
</Form.Label>
<Col xs={fieldXS} xl={fieldXL}>
<RatingStars
value={formik.values.rating ?? undefined}
onSetRating={(value) => formik.setFieldValue("rating", value)}
/>
</Col>
</Form.Group>
{renderStashIDs()} {renderStashIDs()}
{renderButtons()} {renderButtons()}

View File

@@ -72,6 +72,14 @@
right: 1rem; right: 1rem;
width: 3rem; width: 3rem;
} }
.favorite {
color: #ff7373;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9));
position: absolute;
right: 5px;
top: 10px;
}
} }
.card { .card {

View File

@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp, library } from "@fortawesome/fontawesome-svg-core"; import { IconProp, SizeProp, library } from "@fortawesome/fontawesome-svg-core";
import { faStar as fasStar } from "@fortawesome/free-solid-svg-icons"; import { faStar as fasStar } from "@fortawesome/free-solid-svg-icons";
import { faStar as farStar } from "@fortawesome/free-regular-svg-icons"; import { faStar as farStar } from "@fortawesome/free-regular-svg-icons";
@@ -11,13 +11,15 @@ interface IIcon {
icon: IconProp; icon: IconProp;
className?: string; className?: string;
color?: string; color?: string;
size?: SizeProp;
} }
const Icon: React.FC<IIcon> = ({ icon, className, color }) => ( const Icon: React.FC<IIcon> = ({ icon, className, color, size }) => (
<FontAwesomeIcon <FontAwesomeIcon
icon={icon} icon={icon}
className={`fa-icon ${className}`} className={`fa-icon ${className}`}
color={color} color={color}
size={size}
/> />
); );

View File

@@ -45,6 +45,21 @@ function maybeRenderChildren(studio: GQL.StudioDataFragment) {
} }
} }
function maybeRenderRatingBanner(studio: GQL.StudioDataFragment) {
if (!studio.rating) {
return;
}
return (
<div
className={`rating-banner ${
studio.rating ? `rating-${studio.rating}` : ""
}`}
>
RATING: {studio.rating}
</div>
);
}
export const StudioCard: React.FC<IProps> = ({ export const StudioCard: React.FC<IProps> = ({
studio, studio,
hideParent, hideParent,
@@ -122,6 +137,7 @@ export const StudioCard: React.FC<IProps> = ({
</h5> </h5>
{maybeRenderParent(studio, hideParent)} {maybeRenderParent(studio, hideParent)}
{maybeRenderChildren(studio)} {maybeRenderChildren(studio)}
{maybeRenderRatingBanner(studio)}
{maybeRenderPopoverButtonGroup()} {maybeRenderPopoverButtonGroup()}
</> </>
} }

View File

@@ -20,6 +20,7 @@ import {
StudioSelect, StudioSelect,
} from "src/components/Shared"; } from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
import { StudioScenesPanel } from "./StudioScenesPanel"; import { StudioScenesPanel } from "./StudioScenesPanel";
import { StudioGalleriesPanel } from "./StudioGalleriesPanel"; import { StudioGalleriesPanel } from "./StudioGalleriesPanel";
import { StudioImagesPanel } from "./StudioImagesPanel"; import { StudioImagesPanel } from "./StudioImagesPanel";
@@ -45,6 +46,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 [rating, setRating] = useState<number | undefined>(undefined);
const [details, setDetails] = useState<string>(); const [details, setDetails] = useState<string>();
// Studio state // Studio state
@@ -64,6 +66,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);
setRating(state.rating ?? undefined);
setDetails(state.details ?? undefined); setDetails(state.details ?? undefined);
} }
@@ -72,6 +75,7 @@ export const Studio: React.FC = () => {
updateStudioEditState(studioData); updateStudioEditState(studioData);
setImagePreview(studioData.image_path ?? undefined); setImagePreview(studioData.image_path ?? undefined);
setStudio(studioData); setStudio(studioData);
setRating(studioData.rating ?? undefined);
} }
// set up hotkeys // set up hotkeys
@@ -83,6 +87,30 @@ export const Studio: React.FC = () => {
Mousetrap.bind("e", () => setIsEditing(true)); Mousetrap.bind("e", () => setIsEditing(true));
Mousetrap.bind("d d", () => onDelete()); Mousetrap.bind("d d", () => onDelete());
// numeric keypresses get caught by jwplayer, so blur the element
// if the rating sequence is started
Mousetrap.bind("r", () => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
Mousetrap.bind("0", () => setRating(NaN));
Mousetrap.bind("1", () => setRating(1));
Mousetrap.bind("2", () => setRating(2));
Mousetrap.bind("3", () => setRating(3));
Mousetrap.bind("4", () => setRating(4));
Mousetrap.bind("5", () => setRating(5));
setTimeout(() => {
Mousetrap.unbind("0");
Mousetrap.unbind("1");
Mousetrap.unbind("2");
Mousetrap.unbind("3");
Mousetrap.unbind("4");
Mousetrap.unbind("5");
}, 1000);
});
return () => { return () => {
if (isEditing) { if (isEditing) {
Mousetrap.unbind("s s"); Mousetrap.unbind("s s");
@@ -121,6 +149,7 @@ export const Studio: React.FC = () => {
image, image,
details, details,
parent_id: parentStudioId ?? null, parent_id: parentStudioId ?? null,
rating: rating ?? null,
}; };
if (!isNew) { if (!isNew) {
@@ -314,6 +343,16 @@ export const Studio: React.FC = () => {
<td>Parent Studio</td> <td>Parent Studio</td>
<td>{renderStudio()}</td> <td>{renderStudio()}</td>
</tr> </tr>
<tr>
<td>Rating:</td>
<td>
<RatingStars
value={rating}
disabled={!isEditing}
onSetRating={(value) => setRating(value ?? NaN)}
/>
</td>
</tr>
{!isEditing && renderStashIDs()} {!isEditing && renderStashIDs()}
</tbody> </tbody>
</Table> </Table>

View File

@@ -67,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
rating (integer)
details details
``` ```
@@ -77,6 +78,7 @@ url
image (base64 encoding of the image file) image (base64 encoding of the image file)
created_at created_at
updated_at updated_at
rating (integer)
details details
``` ```

View File

@@ -202,6 +202,7 @@ export class ListFilterModel {
"scenes_count", "scenes_count",
"tag_count", "tag_count",
"random", "random",
"rating",
]; ];
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List]; this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
@@ -231,6 +232,7 @@ export class ListFilterModel {
new GenderCriterionOption(), new GenderCriterionOption(),
new PerformerIsMissingCriterionOption(), new PerformerIsMissingCriterionOption(),
new TagsCriterionOption(), new TagsCriterionOption(),
new RatingCriterionOption(),
ListFilterModel.createCriterionOption("url"), ListFilterModel.createCriterionOption("url"),
ListFilterModel.createCriterionOption("tag_count"), ListFilterModel.createCriterionOption("tag_count"),
ListFilterModel.createCriterionOption("scene_count"), ListFilterModel.createCriterionOption("scene_count"),
@@ -244,6 +246,8 @@ export class ListFilterModel {
break; break;
} }
case FilterMode.Studios: case FilterMode.Studios:
this.sortBy = "name";
this.sortByOptions = ["name", "random", "rating", "scenes_count"];
this.sortBy = defaultSort ?? "name"; this.sortBy = defaultSort ?? "name";
this.sortByOptions = [ this.sortByOptions = [
"name", "name",
@@ -257,6 +261,7 @@ export class ListFilterModel {
new NoneCriterionOption(), new NoneCriterionOption(),
new ParentStudiosCriterionOption(), new ParentStudiosCriterionOption(),
new StudioIsMissingCriterionOption(), new StudioIsMissingCriterionOption(),
new RatingCriterionOption(),
ListFilterModel.createCriterionOption("scene_count"), ListFilterModel.createCriterionOption("scene_count"),
ListFilterModel.createCriterionOption("image_count"), ListFilterModel.createCriterionOption("image_count"),
ListFilterModel.createCriterionOption("gallery_count"), ListFilterModel.createCriterionOption("gallery_count"),
@@ -777,6 +782,14 @@ export class ListFilterModel {
}; };
break; break;
} }
case "rating": {
const ratingCrit = criterion as RatingCriterion;
result.rating = {
value: ratingCrit.value,
modifier: ratingCrit.modifier,
};
break;
}
case "url": { case "url": {
const urlCrit = criterion as StringCriterion; const urlCrit = criterion as StringCriterion;
result.url = { result.url = {
@@ -1040,6 +1053,14 @@ export class ListFilterModel {
}; };
break; break;
} }
case "rating": {
const ratingCrit = criterion as RatingCriterion;
result.rating = {
value: ratingCrit.value,
modifier: ratingCrit.modifier,
};
break;
}
case "url": { case "url": {
const urlCrit = criterion as StringCriterion; const urlCrit = criterion as StringCriterion;
result.url = { result.url = {