Studio Performers page (#1405)

* Refactor performer filter
* Add performer studio criterion
* Add Studio Performers page
This commit is contained in:
WithoutPants
2021-05-22 17:07:03 +10:00
committed by GitHub
parent 586d146fdb
commit 33999d3e93
11 changed files with 548 additions and 142 deletions

View File

@@ -29,6 +29,10 @@ enum ResolutionEnum {
} }
input PerformerFilterType { input PerformerFilterType {
AND: PerformerFilterType
OR: PerformerFilterType
NOT: PerformerFilterType
"""Filter by favorite""" """Filter by favorite"""
filter_favorites: Boolean filter_favorites: Boolean
"""Filter by birth year""" """Filter by birth year"""
@@ -81,6 +85,8 @@ input PerformerFilterType {
weight: IntCriterionInput weight: IntCriterionInput
"""Filter by death year""" """Filter by death year"""
death_year: IntCriterionInput death_year: IntCriterionInput
"""Filter by studios where performer appears in scene/image/gallery"""
studios: MultiCriterionInput
} }
input SceneMarkerFilterType { input SceneMarkerFilterType {

View File

@@ -192,6 +192,100 @@ func (qb *performerQueryBuilder) QueryForAutoTag(words []string) ([]*models.Perf
return qb.queryPerformers(query+" WHERE "+where, args) return qb.queryPerformers(query+" WHERE "+where, args)
} }
func (qb *performerQueryBuilder) validateFilter(filter *models.PerformerFilterType) error {
const and = "AND"
const or = "OR"
const not = "NOT"
if filter.And != nil {
if filter.Or != nil {
return illegalFilterCombination(and, or)
}
if filter.Not != nil {
return illegalFilterCombination(and, not)
}
return qb.validateFilter(filter.And)
}
if filter.Or != nil {
if filter.Not != nil {
return illegalFilterCombination(or, not)
}
return qb.validateFilter(filter.Or)
}
if filter.Not != nil {
return qb.validateFilter(filter.Not)
}
return nil
}
func (qb *performerQueryBuilder) makeFilter(filter *models.PerformerFilterType) *filterBuilder {
query := &filterBuilder{}
if filter.And != nil {
query.and(qb.makeFilter(filter.And))
}
if filter.Or != nil {
query.or(qb.makeFilter(filter.Or))
}
if filter.Not != nil {
query.not(qb.makeFilter(filter.Not))
}
const tableName = performerTable
query.handleCriterionFunc(boolCriterionHandler(filter.FilterFavorites, tableName+".favorite"))
query.handleCriterionFunc(yearFilterCriterionHandler(filter.BirthYear, tableName+".birthdate"))
query.handleCriterionFunc(yearFilterCriterionHandler(filter.DeathYear, tableName+".death_date"))
query.handleCriterionFunc(performerAgeFilterCriterionHandler(filter.Age))
query.handleCriterionFunc(func(f *filterBuilder) {
if gender := filter.Gender; gender != nil {
f.addWhere(tableName+".gender = ?", gender.Value.String())
}
})
query.handleCriterionFunc(performerIsMissingCriterionHandler(qb, filter.IsMissing))
query.handleCriterionFunc(stringCriterionHandler(filter.Ethnicity, tableName+".ethnicity"))
query.handleCriterionFunc(stringCriterionHandler(filter.Country, tableName+".country"))
query.handleCriterionFunc(stringCriterionHandler(filter.EyeColor, tableName+".eye_color"))
query.handleCriterionFunc(stringCriterionHandler(filter.Height, tableName+".height"))
query.handleCriterionFunc(stringCriterionHandler(filter.Measurements, tableName+".measurements"))
query.handleCriterionFunc(stringCriterionHandler(filter.FakeTits, tableName+".fake_tits"))
query.handleCriterionFunc(stringCriterionHandler(filter.CareerLength, tableName+".career_length"))
query.handleCriterionFunc(stringCriterionHandler(filter.Tattoos, tableName+".tattoos"))
query.handleCriterionFunc(stringCriterionHandler(filter.Piercings, tableName+".piercings"))
query.handleCriterionFunc(intCriterionHandler(filter.Rating, tableName+".rating"))
query.handleCriterionFunc(stringCriterionHandler(filter.HairColor, tableName+".hair_color"))
query.handleCriterionFunc(stringCriterionHandler(filter.URL, tableName+".url"))
query.handleCriterionFunc(intCriterionHandler(filter.Weight, tableName+".weight"))
query.handleCriterionFunc(func(f *filterBuilder) {
if filter.StashID != nil {
qb.stashIDRepository().join(f, "performer_stash_ids", "performers.id")
stringCriterionHandler(filter.StashID, "performer_stash_ids.stash_id")(f)
}
})
// TODO - need better handling of aliases
query.handleCriterionFunc(stringCriterionHandler(filter.Aliases, tableName+".aliases"))
query.handleCriterionFunc(performerTagsCriterionHandler(qb, filter.Tags))
query.handleCriterionFunc(performerStudiosCriterionHandler(filter.Studios))
query.handleCriterionFunc(performerTagCountCriterionHandler(qb, filter.TagCount))
query.handleCriterionFunc(performerSceneCountCriterionHandler(qb, filter.SceneCount))
query.handleCriterionFunc(performerImageCountCriterionHandler(qb, filter.ImageCount))
query.handleCriterionFunc(performerGalleryCountCriterionHandler(qb, filter.GalleryCount))
return query
}
func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) { func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) {
if performerFilter == nil { if performerFilter == nil {
performerFilter = &models.PerformerFilterType{} performerFilter = &models.PerformerFilterType{}
@@ -204,11 +298,6 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy
query := qb.newQuery() query := qb.newQuery()
query.body = selectDistinctIDs(tableName) query.body = selectDistinctIDs(tableName)
query.body += `
left join performers_scenes as scenes_join on scenes_join.performer_id = performers.id
left join scenes on scenes_join.scene_id = scenes.id
left join performer_stash_ids on performer_stash_ids.performer_id = performers.id
`
if q := findFilter.Q; q != nil && *q != "" { if q := findFilter.Q; q != nil && *q != "" {
searchColumns := []string{"performers.name", "performers.aliases"} searchColumns := []string{"performers.name", "performers.aliases"}
@@ -217,86 +306,12 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy
query.addArg(thisArgs...) query.addArg(thisArgs...)
} }
if favoritesFilter := performerFilter.FilterFavorites; favoritesFilter != nil { if err := qb.validateFilter(performerFilter); err != nil {
var favStr string return nil, 0, err
if *favoritesFilter == true {
favStr = "1"
} else {
favStr = "0"
}
query.addWhere("performers.favorite = " + favStr)
} }
filter := qb.makeFilter(performerFilter)
if birthYear := performerFilter.BirthYear; birthYear != nil { query.addFilter(filter)
clauses, thisArgs := getYearFilterClause(birthYear.Modifier, birthYear.Value, "birthdate")
query.addWhere(clauses...)
query.addArg(thisArgs...)
}
if deathYear := performerFilter.DeathYear; deathYear != nil {
clauses, thisArgs := getYearFilterClause(deathYear.Modifier, deathYear.Value, "death_date")
query.addWhere(clauses...)
query.addArg(thisArgs...)
}
if age := performerFilter.Age; age != nil {
clauses, thisArgs := getAgeFilterClause(age.Modifier, age.Value)
query.addWhere(clauses...)
query.addArg(thisArgs...)
}
if gender := performerFilter.Gender; gender != nil {
query.addWhere("performers.gender = ?")
query.addArg(gender.Value.String())
}
if isMissingFilter := performerFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
switch *isMissingFilter {
case "scenes":
query.addWhere("scenes_join.scene_id IS NULL")
case "image":
query.body += `left join performers_image on performers_image.performer_id = performers.id
`
query.addWhere("performers_image.performer_id IS NULL")
default:
query.addWhere("(performers." + *isMissingFilter + " IS NULL OR TRIM(performers." + *isMissingFilter + ") = '')")
}
}
query.handleStringCriterionInput(performerFilter.Ethnicity, tableName+".ethnicity")
query.handleStringCriterionInput(performerFilter.Country, tableName+".country")
query.handleStringCriterionInput(performerFilter.EyeColor, tableName+".eye_color")
query.handleStringCriterionInput(performerFilter.Height, tableName+".height")
query.handleStringCriterionInput(performerFilter.Measurements, tableName+".measurements")
query.handleStringCriterionInput(performerFilter.FakeTits, tableName+".fake_tits")
query.handleStringCriterionInput(performerFilter.CareerLength, tableName+".career_length")
query.handleStringCriterionInput(performerFilter.Tattoos, tableName+".tattoos")
query.handleStringCriterionInput(performerFilter.Piercings, tableName+".piercings")
query.handleIntCriterionInput(performerFilter.Rating, tableName+".rating")
query.handleStringCriterionInput(performerFilter.HairColor, tableName+".hair_color")
query.handleStringCriterionInput(performerFilter.URL, tableName+".url")
query.handleIntCriterionInput(performerFilter.Weight, tableName+".weight")
query.handleStringCriterionInput(performerFilter.StashID, "performer_stash_ids.stash_id")
// TODO - need better handling of aliases
query.handleStringCriterionInput(performerFilter.Aliases, tableName+".aliases")
if tagsFilter := performerFilter.Tags; tagsFilter != nil && len(tagsFilter.Value) > 0 {
for _, tagID := range tagsFilter.Value {
query.addArg(tagID)
}
query.body += ` left join performers_tags as tags_join on tags_join.performer_id = performers.id
LEFT JOIN tags on tags_join.tag_id = tags.id`
whereClause, havingClause := getMultiCriterionClause("performers", "tags", "performers_tags", "performer_id", "tag_id", tagsFilter)
query.addWhere(whereClause)
query.addHaving(havingClause)
}
query.handleCountCriterion(performerFilter.TagCount, performerTable, performersTagsTable, performerIDColumn)
query.handleCountCriterion(performerFilter.SceneCount, performerTable, performersScenesTable, performerIDColumn)
query.handleCountCriterion(performerFilter.ImageCount, performerTable, performersImagesTable, performerIDColumn)
query.handleCountCriterion(performerFilter.GalleryCount, performerTable, performersGalleriesTable, performerIDColumn)
query.sortAndPagination = qb.getPerformerSort(findFilter) + getPagination(findFilter) query.sortAndPagination = qb.getPerformerSort(findFilter) + getPagination(findFilter)
idsResult, countResult, err := query.executeFind() idsResult, countResult, err := query.executeFind()
@@ -316,65 +331,167 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy
return performers, countResult, nil return performers, countResult, nil
} }
func getYearFilterClause(criterionModifier models.CriterionModifier, value int, col string) ([]string, []interface{}) { func performerIsMissingCriterionHandler(qb *performerQueryBuilder, isMissing *string) criterionHandlerFunc {
var clauses []string return func(f *filterBuilder) {
var args []interface{} if isMissing != nil && *isMissing != "" {
switch *isMissing {
yearStr := strconv.Itoa(value) case "scenes":
startOfYear := yearStr + "-01-01" f.addJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id")
endOfYear := yearStr + "-12-31" f.addWhere("scenes_join.scene_id IS NULL")
case "image":
if modifier := criterionModifier.String(); criterionModifier.IsValid() { f.addJoin(performersImagesTable, "", "performers_image.performer_id = performers.id")
switch modifier { f.addWhere("performers_image.performer_id IS NULL")
case "EQUALS": default:
// between yyyy-01-01 and yyyy-12-31 f.addWhere("(performers." + *isMissing + " IS NULL OR TRIM(performers." + *isMissing + ") = '')")
clauses = append(clauses, "performers."+col+" >= ?") }
clauses = append(clauses, "performers."+col+" <= ?")
args = append(args, startOfYear)
args = append(args, endOfYear)
case "NOT_EQUALS":
// outside of yyyy-01-01 to yyyy-12-31
clauses = append(clauses, "performers."+col+" < ? OR performers."+col+" > ?")
args = append(args, startOfYear)
args = append(args, endOfYear)
case "GREATER_THAN":
// > yyyy-12-31
clauses = append(clauses, "performers."+col+" > ?")
args = append(args, endOfYear)
case "LESS_THAN":
// < yyyy-01-01
clauses = append(clauses, "performers."+col+" < ?")
args = append(args, startOfYear)
} }
} }
return clauses, args
} }
func getAgeFilterClause(criterionModifier models.CriterionModifier, value int) ([]string, []interface{}) { func yearFilterCriterionHandler(year *models.IntCriterionInput, col string) criterionHandlerFunc {
var clauses []string return func(f *filterBuilder) {
var args []interface{} if year != nil && year.Modifier.IsValid() {
var clause string yearStr := strconv.Itoa(year.Value)
startOfYear := yearStr + "-01-01"
endOfYear := yearStr + "-12-31"
if criterionModifier.IsValid() { switch year.Modifier {
switch criterionModifier { case models.CriterionModifierEquals:
case models.CriterionModifierEquals: // between yyyy-01-01 and yyyy-12-31
clause = " == ?" f.addWhere(col+" >= ?", startOfYear)
case models.CriterionModifierNotEquals: f.addWhere(col+" <= ?", endOfYear)
clause = " != ?" case models.CriterionModifierNotEquals:
case models.CriterionModifierGreaterThan: // outside of yyyy-01-01 to yyyy-12-31
clause = " > ?" f.addWhere(col+" < ? OR "+col+" > ?", startOfYear, endOfYear)
case models.CriterionModifierLessThan: case models.CriterionModifierGreaterThan:
clause = " < ?" // > yyyy-12-31
} f.addWhere(col+" > ?", endOfYear)
case models.CriterionModifierLessThan:
if clause != "" { // < yyyy-01-01
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) f.addWhere(col+" < ?", startOfYear)
args = append(args, value) }
} }
} }
}
return clauses, args func performerAgeFilterCriterionHandler(age *models.IntCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) {
if age != nil && age.Modifier.IsValid() {
var op string
switch age.Modifier {
case models.CriterionModifierEquals:
op = "=="
case models.CriterionModifierNotEquals:
op = "!="
case models.CriterionModifierGreaterThan:
op = ">"
case models.CriterionModifierLessThan:
op = "<"
}
if op != "" {
f.addWhere("cast(IFNULL(strftime('%Y.%m%d', performers.death_date), strftime('%Y.%m%d', 'now')) - strftime('%Y.%m%d', performers.birthdate) as int) "+op+" ?", age.Value)
}
}
}
}
func performerTagsCriterionHandler(qb *performerQueryBuilder, tags *models.MultiCriterionInput) criterionHandlerFunc {
h := joinedMultiCriterionHandlerBuilder{
primaryTable: performerTable,
joinTable: performersTagsTable,
joinAs: "tags_join",
primaryFK: performerIDColumn,
foreignFK: tagIDColumn,
addJoinTable: func(f *filterBuilder) {
qb.tagsRepository().join(f, "tags_join", "performers.id")
},
}
return h.handler(tags)
}
func performerTagCountCriterionHandler(qb *performerQueryBuilder, count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: performerTable,
joinTable: performersTagsTable,
primaryFK: performerIDColumn,
}
return h.handler(count)
}
func performerSceneCountCriterionHandler(qb *performerQueryBuilder, count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: performerTable,
joinTable: performersScenesTable,
primaryFK: performerIDColumn,
}
return h.handler(count)
}
func performerImageCountCriterionHandler(qb *performerQueryBuilder, count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: performerTable,
joinTable: performersImagesTable,
primaryFK: performerIDColumn,
}
return h.handler(count)
}
func performerGalleryCountCriterionHandler(qb *performerQueryBuilder, count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: performerTable,
joinTable: performersGalleriesTable,
primaryFK: performerIDColumn,
}
return h.handler(count)
}
func performerStudiosCriterionHandler(studios *models.MultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) {
if studios != nil {
var countCondition string
var clauseJoin string
if studios.Modifier == models.CriterionModifierIncludes {
// return performers who appear in scenes/images/galleries with any of the given studios
countCondition = " > 0"
clauseJoin = " OR "
} else if studios.Modifier == models.CriterionModifierExcludes {
// exclude performers who appear in scenes/images/galleries with any of the given studios
countCondition = " = 0"
clauseJoin = " AND "
} else {
return
}
templStr := "(SELECT COUNT(DISTINCT %[1]s.id) FROM %[1]s LEFT JOIN %[2]s ON %[1]s.id = %[2]s.%[3]s WHERE %[2]s.performer_id = performers.id AND %[1]s.studio_id IN %[4]s)" + countCondition
inBinding := getInBinding(len(studios.Value))
clauses := []string{
fmt.Sprintf(templStr, sceneTable, performersScenesTable, sceneIDColumn, inBinding),
fmt.Sprintf(templStr, imageTable, performersImagesTable, imageIDColumn, inBinding),
fmt.Sprintf(templStr, galleryTable, performersGalleriesTable, galleryIDColumn, inBinding),
}
var args []interface{}
for _, tagID := range studios.Value {
args = append(args, tagID)
}
// this is a bit gross. We need the args three times
combinedArgs := append(args, append(args, args...)...)
f.addWhere(fmt.Sprintf("(%s)", strings.Join(clauses, clauseJoin)), combinedArgs...)
}
}
} }
func (qb *performerQueryBuilder) getPerformerSort(findFilter *models.FindFilterType) string { func (qb *performerQueryBuilder) getPerformerSort(findFilter *models.FindFilterType) string {

View File

@@ -100,6 +100,143 @@ func TestPerformerFindByNames(t *testing.T) {
}) })
} }
func TestPerformerQueryEthnicityOr(t *testing.T) {
const performer1Idx = 1
const performer2Idx = 2
performer1Eth := getPerformerStringValue(performer1Idx, "Ethnicity")
performer2Eth := getPerformerStringValue(performer2Idx, "Ethnicity")
performerFilter := models.PerformerFilterType{
Ethnicity: &models.StringCriterionInput{
Value: performer1Eth,
Modifier: models.CriterionModifierEquals,
},
Or: &models.PerformerFilterType{
Ethnicity: &models.StringCriterionInput{
Value: performer2Eth,
Modifier: models.CriterionModifierEquals,
},
},
}
withTxn(func(r models.Repository) error {
sqb := r.Performer()
performers := queryPerformers(t, sqb, &performerFilter, nil)
assert.Len(t, performers, 2)
assert.Equal(t, performer1Eth, performers[0].Ethnicity.String)
assert.Equal(t, performer2Eth, performers[1].Ethnicity.String)
return nil
})
}
func TestPerformerQueryEthnicityAndRating(t *testing.T) {
const performerIdx = 1
performerEth := getPerformerStringValue(performerIdx, "Ethnicity")
performerRating := getRating(performerIdx)
performerFilter := models.PerformerFilterType{
Ethnicity: &models.StringCriterionInput{
Value: performerEth,
Modifier: models.CriterionModifierEquals,
},
And: &models.PerformerFilterType{
Rating: &models.IntCriterionInput{
Value: int(performerRating.Int64),
Modifier: models.CriterionModifierEquals,
},
},
}
withTxn(func(r models.Repository) error {
sqb := r.Performer()
performers := queryPerformers(t, sqb, &performerFilter, nil)
assert.Len(t, performers, 1)
assert.Equal(t, performerEth, performers[0].Ethnicity.String)
assert.Equal(t, performerRating.Int64, performers[0].Rating.Int64)
return nil
})
}
func TestPerformerQueryPathNotRating(t *testing.T) {
const performerIdx = 1
performerRating := getRating(performerIdx)
ethCriterion := models.StringCriterionInput{
Value: "performer_.*1_Ethnicity",
Modifier: models.CriterionModifierMatchesRegex,
}
ratingCriterion := models.IntCriterionInput{
Value: int(performerRating.Int64),
Modifier: models.CriterionModifierEquals,
}
performerFilter := models.PerformerFilterType{
Ethnicity: &ethCriterion,
Not: &models.PerformerFilterType{
Rating: &ratingCriterion,
},
}
withTxn(func(r models.Repository) error {
sqb := r.Performer()
performers := queryPerformers(t, sqb, &performerFilter, nil)
for _, performer := range performers {
verifyString(t, performer.Ethnicity.String, ethCriterion)
ratingCriterion.Modifier = models.CriterionModifierNotEquals
verifyInt64(t, performer.Rating, ratingCriterion)
}
return nil
})
}
func TestPerformerIllegalQuery(t *testing.T) {
assert := assert.New(t)
const performerIdx = 1
subFilter := models.PerformerFilterType{
Ethnicity: &models.StringCriterionInput{
Value: getPerformerStringValue(performerIdx, "Ethnicity"),
Modifier: models.CriterionModifierEquals,
},
}
performerFilter := &models.PerformerFilterType{
And: &subFilter,
Or: &subFilter,
}
withTxn(func(r models.Repository) error {
sqb := r.Performer()
_, _, err := sqb.Query(performerFilter, nil)
assert.NotNil(err)
performerFilter.Or = nil
performerFilter.Not = &subFilter
_, _, err = sqb.Query(performerFilter, nil)
assert.NotNil(err)
performerFilter.And = nil
performerFilter.Or = &subFilter
_, _, err = sqb.Query(performerFilter, nil)
assert.NotNil(err)
return nil
})
}
func TestPerformerQueryForAutoTag(t *testing.T) { func TestPerformerQueryForAutoTag(t *testing.T) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
tqb := r.Performer() tqb := r.Performer()
@@ -595,6 +732,58 @@ func verifyPerformersGalleryCount(t *testing.T, galleryCountCriterion models.Int
}) })
} }
func TestPerformerQueryStudio(t *testing.T) {
withTxn(func(r models.Repository) error {
testCases := []struct {
studioIndex int
performerIndex int
}{
{studioIndex: studioIdxWithScenePerformer, performerIndex: performerIdxWithSceneStudio},
{studioIndex: studioIdxWithImagePerformer, performerIndex: performerIdxWithImageStudio},
{studioIndex: studioIdxWithGalleryPerformer, performerIndex: performerIdxWithGalleryStudio},
}
sqb := r.Performer()
for _, tc := range testCases {
studioCriterion := models.MultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[tc.studioIndex]),
},
Modifier: models.CriterionModifierIncludes,
}
performerFilter := models.PerformerFilterType{
Studios: &studioCriterion,
}
performers := queryPerformers(t, sqb, &performerFilter, nil)
assert.Len(t, performers, 1)
// ensure id is correct
assert.Equal(t, performerIDs[tc.performerIndex], performers[0].ID)
studioCriterion = models.MultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[tc.studioIndex]),
},
Modifier: models.CriterionModifierExcludes,
}
q := getPerformerStringValue(tc.performerIndex, "Name")
findFilter := models.FindFilterType{
Q: &q,
}
performers = queryPerformers(t, sqb, &performerFilter, &findFilter)
assert.Len(t, performers, 0)
}
return nil
})
}
func TestPerformerStashIDs(t *testing.T) { func TestPerformerStashIDs(t *testing.T) {
if err := withTxn(func(r models.Repository) error { if err := withTxn(func(r models.Repository) error {
qb := r.Performer() qb := r.Performer()

View File

@@ -40,6 +40,7 @@ const (
sceneIdxWithPerformerTag sceneIdxWithPerformerTag
sceneIdxWithPerformerTwoTags sceneIdxWithPerformerTwoTags
sceneIdxWithSpacedName sceneIdxWithSpacedName
sceneIdxWithStudioPerformer
// new indexes above // new indexes above
lastSceneIdx lastSceneIdx
@@ -60,6 +61,7 @@ const (
imageIdxWithStudio imageIdxWithStudio
imageIdx1WithStudio imageIdx1WithStudio
imageIdx2WithStudio imageIdx2WithStudio
imageIdxWithStudioPerformer
imageIdxInZip // TODO - not implemented imageIdxInZip // TODO - not implemented
imageIdxWithPerformerTag imageIdxWithPerformerTag
imageIdxWithPerformerTwoTags imageIdxWithPerformerTwoTags
@@ -82,6 +84,9 @@ const (
performerIdxWithTwoGalleries performerIdxWithTwoGalleries
performerIdx1WithGallery performerIdx1WithGallery
performerIdx2WithGallery performerIdx2WithGallery
performerIdxWithSceneStudio
performerIdxWithImageStudio
performerIdxWithGalleryStudio
// new indexes above // new indexes above
// performers with dup names start from the end // performers with dup names start from the end
performerIdx1WithDupName performerIdx1WithDupName
@@ -119,6 +124,7 @@ const (
galleryIdx2WithStudio galleryIdx2WithStudio
galleryIdxWithPerformerTag galleryIdxWithPerformerTag
galleryIdxWithPerformerTwoTags galleryIdxWithPerformerTwoTags
galleryIdxWithStudioPerformer
// new indexes above // new indexes above
lastGalleryIdx lastGalleryIdx
@@ -160,6 +166,9 @@ const (
studioIdxWithTwoImages studioIdxWithTwoImages
studioIdxWithGallery studioIdxWithGallery
studioIdxWithTwoGalleries studioIdxWithTwoGalleries
studioIdxWithScenePerformer
studioIdxWithImagePerformer
studioIdxWithGalleryPerformer
// new indexes above // new indexes above
// studios with dup names start from the end // studios with dup names start from the end
studioIdxWithDupName studioIdxWithDupName
@@ -216,6 +225,7 @@ var (
{sceneIdxWithPerformerTwoTags, performerIdxWithTwoTags}, {sceneIdxWithPerformerTwoTags, performerIdxWithTwoTags},
{sceneIdx1WithPerformer, performerIdxWithTwoScenes}, {sceneIdx1WithPerformer, performerIdxWithTwoScenes},
{sceneIdx2WithPerformer, performerIdxWithTwoScenes}, {sceneIdx2WithPerformer, performerIdxWithTwoScenes},
{sceneIdxWithStudioPerformer, performerIdxWithSceneStudio},
} }
sceneGalleryLinks = [][2]int{ sceneGalleryLinks = [][2]int{
@@ -230,6 +240,7 @@ var (
{sceneIdxWithStudio, studioIdxWithScene}, {sceneIdxWithStudio, studioIdxWithScene},
{sceneIdx1WithStudio, studioIdxWithTwoScenes}, {sceneIdx1WithStudio, studioIdxWithTwoScenes},
{sceneIdx2WithStudio, studioIdxWithTwoScenes}, {sceneIdx2WithStudio, studioIdxWithTwoScenes},
{sceneIdxWithStudioPerformer, studioIdxWithScenePerformer},
} }
) )
@@ -245,6 +256,7 @@ var (
{imageIdxWithStudio, studioIdxWithImage}, {imageIdxWithStudio, studioIdxWithImage},
{imageIdx1WithStudio, studioIdxWithTwoImages}, {imageIdx1WithStudio, studioIdxWithTwoImages},
{imageIdx2WithStudio, studioIdxWithTwoImages}, {imageIdx2WithStudio, studioIdxWithTwoImages},
{imageIdxWithStudioPerformer, studioIdxWithImagePerformer},
} }
imageTagLinks = [][2]int{ imageTagLinks = [][2]int{
{imageIdxWithTag, tagIdxWithImage}, {imageIdxWithTag, tagIdxWithImage},
@@ -259,6 +271,7 @@ var (
{imageIdxWithPerformerTwoTags, performerIdxWithTwoTags}, {imageIdxWithPerformerTwoTags, performerIdxWithTwoTags},
{imageIdx1WithPerformer, performerIdxWithTwoImages}, {imageIdx1WithPerformer, performerIdxWithTwoImages},
{imageIdx2WithPerformer, performerIdxWithTwoImages}, {imageIdx2WithPerformer, performerIdxWithTwoImages},
{imageIdxWithStudioPerformer, performerIdxWithImageStudio},
} }
) )
@@ -271,12 +284,14 @@ var (
{galleryIdxWithPerformerTwoTags, performerIdxWithTwoTags}, {galleryIdxWithPerformerTwoTags, performerIdxWithTwoTags},
{galleryIdx1WithPerformer, performerIdxWithTwoGalleries}, {galleryIdx1WithPerformer, performerIdxWithTwoGalleries},
{galleryIdx2WithPerformer, performerIdxWithTwoGalleries}, {galleryIdx2WithPerformer, performerIdxWithTwoGalleries},
{galleryIdxWithStudioPerformer, performerIdxWithGalleryStudio},
} }
galleryStudioLinks = [][2]int{ galleryStudioLinks = [][2]int{
{galleryIdxWithStudio, studioIdxWithGallery}, {galleryIdxWithStudio, studioIdxWithGallery},
{galleryIdx1WithStudio, studioIdxWithTwoGalleries}, {galleryIdx1WithStudio, studioIdxWithTwoGalleries},
{galleryIdx2WithStudio, studioIdxWithTwoGalleries}, {galleryIdx2WithStudio, studioIdxWithTwoGalleries},
{galleryIdxWithStudioPerformer, studioIdxWithGalleryPerformer},
} }
galleryTagLinks = [][2]int{ galleryTagLinks = [][2]int{
@@ -745,6 +760,8 @@ func createPerformers(pqb models.PerformerReaderWriter, n int, o int) error {
}, },
DeathDate: getPerformerDeathDate(i), DeathDate: getPerformerDeathDate(i),
Details: sql.NullString{String: getPerformerStringValue(i, "Details"), Valid: true}, Details: sql.NullString{String: getPerformerStringValue(i, "Details"), Valid: true},
Ethnicity: sql.NullString{String: getPerformerStringValue(i, "Ethnicity"), Valid: true},
Rating: getRating(i),
} }
careerLength := getPerformerCareerLength(i) careerLength := getPerformerCareerLength(i)

View File

@@ -1,7 +1,9 @@
### ✨ New Features ### ✨ New Features
* Added Performers tab to Studio page. ([#1405](https://github.com/stashapp/stash/pull/1405))
* Added [DLNA server](/settings?tab=dlna). ([#1364](https://github.com/stashapp/stash/pull/1364)) * Added [DLNA server](/settings?tab=dlna). ([#1364](https://github.com/stashapp/stash/pull/1364))
### 🎨 Improvements ### 🎨 Improvements
* Add Studios Performer filter criterion. ([#1405](https://github.com/stashapp/stash/pull/1405))
* Add `subtractDays` post-process scraper action. ([#1399](https://github.com/stashapp/stash/pull/1399)) * Add `subtractDays` post-process scraper action. ([#1399](https://github.com/stashapp/stash/pull/1399))
* Skip scanning directories if path matches image and video exclude patterns. ([#1382](https://github.com/stashapp/stash/pull/1382)) * Skip scanning directories if path matches image and video exclude patterns. ([#1382](https://github.com/stashapp/stash/pull/1382))
* Add button to remove studio stash ID. ([#1378](https://github.com/stashapp/stash/pull/1378)) * Add button to remove studio stash ID. ([#1378](https://github.com/stashapp/stash/pull/1378))

View File

@@ -11,14 +11,22 @@ import {
TruncatedText, TruncatedText,
} from "src/components/Shared"; } from "src/components/Shared";
import { Button, ButtonGroup } from "react-bootstrap"; import { Button, ButtonGroup } from "react-bootstrap";
import { Criterion } from "src/models/list-filter/criteria/criterion";
import { PopoverCountButton } from "../Shared/PopoverCountButton"; import { PopoverCountButton } from "../Shared/PopoverCountButton";
export interface IPerformerCardExtraCriteria {
scenes: Criterion[];
images: Criterion[];
galleries: Criterion[];
}
interface IPerformerCardProps { interface IPerformerCardProps {
performer: GQL.PerformerDataFragment; performer: GQL.PerformerDataFragment;
ageFromDate?: string; ageFromDate?: string;
selecting?: boolean; selecting?: boolean;
selected?: boolean; selected?: boolean;
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
extraCriteria?: IPerformerCardExtraCriteria;
} }
export const PerformerCard: React.FC<IPerformerCardProps> = ({ export const PerformerCard: React.FC<IPerformerCardProps> = ({
@@ -27,6 +35,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
selecting, selecting,
selected, selected,
onSelectedChanged, onSelectedChanged,
extraCriteria,
}) => { }) => {
const age = TextUtils.age( const age = TextUtils.age(
performer.birthdate, performer.birthdate,
@@ -52,7 +61,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
<PopoverCountButton <PopoverCountButton
type="scene" type="scene"
count={performer.scene_count} count={performer.scene_count}
url={NavUtils.makePerformerScenesUrl(performer)} url={NavUtils.makePerformerScenesUrl(performer, extraCriteria?.scenes)}
/> />
); );
} }
@@ -64,7 +73,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
<PopoverCountButton <PopoverCountButton
type="image" type="image"
count={performer.image_count} count={performer.image_count}
url={NavUtils.makePerformerImagesUrl(performer)} url={NavUtils.makePerformerImagesUrl(performer, extraCriteria?.images)}
/> />
); );
} }
@@ -76,7 +85,10 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
<PopoverCountButton <PopoverCountButton
type="gallery" type="gallery"
count={performer.gallery_count} count={performer.gallery_count}
url={NavUtils.makePerformerGalleriesUrl(performer)} url={NavUtils.makePerformerGalleriesUrl(
performer,
extraCriteria?.galleries
)}
/> />
); );
} }

View File

@@ -16,18 +16,20 @@ import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types"; import { DisplayMode } from "src/models/list-filter/types";
import { PerformerTagger } from "src/components/Tagger"; import { PerformerTagger } from "src/components/Tagger";
import { ExportDialog, DeleteEntityDialog } from "src/components/Shared"; import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
import { PerformerCard } from "./PerformerCard"; import { IPerformerCardExtraCriteria, PerformerCard } from "./PerformerCard";
import { PerformerListTable } from "./PerformerListTable"; import { PerformerListTable } from "./PerformerListTable";
import { EditPerformersDialog } from "./EditPerformersDialog"; import { EditPerformersDialog } from "./EditPerformersDialog";
interface IPerformerList { interface IPerformerList {
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
persistState?: PersistanceLevel; persistState?: PersistanceLevel;
extraCriteria?: IPerformerCardExtraCriteria;
} }
export const PerformerList: React.FC<IPerformerList> = ({ export const PerformerList: React.FC<IPerformerList> = ({
filterHook, filterHook,
persistState, persistState,
extraCriteria,
}) => { }) => {
const history = useHistory(); const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
@@ -172,6 +174,7 @@ export const PerformerList: React.FC<IPerformerList> = ({
onSelectedChanged={(selected: boolean, shiftKey: boolean) => onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
listData.onSelectChange(p.id, selected, shiftKey) listData.onSelectChange(p.id, selected, shiftKey)
} }
extraCriteria={extraCriteria}
/> />
))} ))}
</div> </div>

View File

@@ -26,6 +26,7 @@ import { StudioScenesPanel } from "./StudioScenesPanel";
import { StudioGalleriesPanel } from "./StudioGalleriesPanel"; import { StudioGalleriesPanel } from "./StudioGalleriesPanel";
import { StudioImagesPanel } from "./StudioImagesPanel"; import { StudioImagesPanel } from "./StudioImagesPanel";
import { StudioChildrenPanel } from "./StudioChildrenPanel"; import { StudioChildrenPanel } from "./StudioChildrenPanel";
import { StudioPerformersPanel } from "./StudioPerformersPanel";
interface IStudioParams { interface IStudioParams {
id?: string; id?: string;
@@ -294,7 +295,10 @@ export const Studio: React.FC = () => {
} }
const activeTabKey = const activeTabKey =
tab === "childstudios" || tab === "images" || tab === "galleries" tab === "childstudios" ||
tab === "images" ||
tab === "galleries" ||
tab === "performers"
? tab ? tab
: "scenes"; : "scenes";
const setActiveTabKey = (newTab: string | null) => { const setActiveTabKey = (newTab: string | null) => {
@@ -416,6 +420,9 @@ export const Studio: React.FC = () => {
<Tab eventKey="images" title="Images"> <Tab eventKey="images" title="Images">
<StudioImagesPanel studio={studio} /> <StudioImagesPanel studio={studio} />
</Tab> </Tab>
<Tab eventKey="performers" title="Performers">
<StudioPerformersPanel studio={studio} />
</Tab>
<Tab eventKey="childstudios" title="Child Studios"> <Tab eventKey="childstudios" title="Child Studios">
<StudioChildrenPanel studio={studio} /> <StudioChildrenPanel studio={studio} />
</Tab> </Tab>

View File

@@ -0,0 +1,31 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
import { studioFilterHook } from "src/core/studios";
import { PerformerList } from "src/components/Performers/PerformerList";
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
interface IStudioPerformersPanel {
studio: Partial<GQL.StudioDataFragment>;
}
export const StudioPerformersPanel: React.FC<IStudioPerformersPanel> = ({
studio,
}) => {
const studioCriterion = new StudiosCriterion();
studioCriterion.value = [
{ id: studio.id!, label: studio.name || `Studio ${studio.id}` },
];
const extraCriteria = {
scenes: [studioCriterion],
images: [studioCriterion],
galleries: [studioCriterion],
};
return (
<PerformerList
filterHook={studioFilterHook(studio)}
extraCriteria={extraCriteria}
/>
);
};

View File

@@ -239,6 +239,7 @@ export class ListFilterModel {
new PerformerIsMissingCriterionOption(), new PerformerIsMissingCriterionOption(),
new TagsCriterionOption(), new TagsCriterionOption(),
new RatingCriterionOption(), new RatingCriterionOption(),
new StudiosCriterionOption(),
ListFilterModel.createCriterionOption("url"), ListFilterModel.createCriterionOption("url"),
ListFilterModel.createCriterionOption("tag_count"), ListFilterModel.createCriterionOption("tag_count"),
ListFilterModel.createCriterionOption("scene_count"), ListFilterModel.createCriterionOption("scene_count"),
@@ -815,6 +816,14 @@ export class ListFilterModel {
}; };
break; break;
} }
case "studios": {
const studCrit = criterion as StudiosCriterion;
result.studios = {
value: studCrit.value.map((studio) => studio.id),
modifier: studCrit.modifier,
};
break;
}
case "tag_count": { case "tag_count": {
const tagCountCrit = criterion as NumberCriterion; const tagCountCrit = criterion as NumberCriterion;
result.tag_count = { result.tag_count = {

View File

@@ -9,9 +9,17 @@ import { TagsCriterion } from "src/models/list-filter/criteria/tags";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { FilterMode } from "src/models/list-filter/types"; import { FilterMode } from "src/models/list-filter/types";
import { MoviesCriterion } from "src/models/list-filter/criteria/movies"; import { MoviesCriterion } from "src/models/list-filter/criteria/movies";
import { Criterion } from "src/models/list-filter/criteria/criterion";
function addExtraCriteria(dest: Criterion[], src?: Criterion[]) {
if (src && src.length > 0) {
dest.push(...src);
}
}
const makePerformerScenesUrl = ( const makePerformerScenesUrl = (
performer: Partial<GQL.PerformerDataFragment> performer: Partial<GQL.PerformerDataFragment>,
extraCriteria?: Criterion[]
) => { ) => {
if (!performer.id) return "#"; if (!performer.id) return "#";
const filter = new ListFilterModel(FilterMode.Scenes); const filter = new ListFilterModel(FilterMode.Scenes);
@@ -20,11 +28,13 @@ const makePerformerScenesUrl = (
{ id: performer.id, label: performer.name || `Performer ${performer.id}` }, { id: performer.id, label: performer.name || `Performer ${performer.id}` },
]; ];
filter.criteria.push(criterion); filter.criteria.push(criterion);
addExtraCriteria(filter.criteria, extraCriteria);
return `/scenes?${filter.makeQueryParameters()}`; return `/scenes?${filter.makeQueryParameters()}`;
}; };
const makePerformerImagesUrl = ( const makePerformerImagesUrl = (
performer: Partial<GQL.PerformerDataFragment> performer: Partial<GQL.PerformerDataFragment>,
extraCriteria?: Criterion[]
) => { ) => {
if (!performer.id) return "#"; if (!performer.id) return "#";
const filter = new ListFilterModel(FilterMode.Images); const filter = new ListFilterModel(FilterMode.Images);
@@ -33,11 +43,13 @@ const makePerformerImagesUrl = (
{ id: performer.id, label: performer.name || `Performer ${performer.id}` }, { id: performer.id, label: performer.name || `Performer ${performer.id}` },
]; ];
filter.criteria.push(criterion); filter.criteria.push(criterion);
addExtraCriteria(filter.criteria, extraCriteria);
return `/images?${filter.makeQueryParameters()}`; return `/images?${filter.makeQueryParameters()}`;
}; };
const makePerformerGalleriesUrl = ( const makePerformerGalleriesUrl = (
performer: Partial<GQL.PerformerDataFragment> performer: Partial<GQL.PerformerDataFragment>,
extraCriteria?: Criterion[]
) => { ) => {
if (!performer.id) return "#"; if (!performer.id) return "#";
const filter = new ListFilterModel(FilterMode.Galleries); const filter = new ListFilterModel(FilterMode.Galleries);
@@ -46,6 +58,7 @@ const makePerformerGalleriesUrl = (
{ id: performer.id, label: performer.name || `Performer ${performer.id}` }, { id: performer.id, label: performer.name || `Performer ${performer.id}` },
]; ];
filter.criteria.push(criterion); filter.criteria.push(criterion);
addExtraCriteria(filter.criteria, extraCriteria);
return `/galleries?${filter.makeQueryParameters()}`; return `/galleries?${filter.makeQueryParameters()}`;
}; };