mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Studio Performers page (#1405)
* Refactor performer filter * Add performer studio criterion * Add Studio Performers page
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
case "scenes":
|
||||||
|
f.addJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id")
|
||||||
|
f.addWhere("scenes_join.scene_id IS NULL")
|
||||||
|
case "image":
|
||||||
|
f.addJoin(performersImagesTable, "", "performers_image.performer_id = performers.id")
|
||||||
|
f.addWhere("performers_image.performer_id IS NULL")
|
||||||
|
default:
|
||||||
|
f.addWhere("(performers." + *isMissing + " IS NULL OR TRIM(performers." + *isMissing + ") = '')")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
yearStr := strconv.Itoa(value)
|
func yearFilterCriterionHandler(year *models.IntCriterionInput, col string) criterionHandlerFunc {
|
||||||
|
return func(f *filterBuilder) {
|
||||||
|
if year != nil && year.Modifier.IsValid() {
|
||||||
|
yearStr := strconv.Itoa(year.Value)
|
||||||
startOfYear := yearStr + "-01-01"
|
startOfYear := yearStr + "-01-01"
|
||||||
endOfYear := yearStr + "-12-31"
|
endOfYear := yearStr + "-12-31"
|
||||||
|
|
||||||
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
|
switch year.Modifier {
|
||||||
switch modifier {
|
case models.CriterionModifierEquals:
|
||||||
case "EQUALS":
|
|
||||||
// between yyyy-01-01 and yyyy-12-31
|
// between yyyy-01-01 and yyyy-12-31
|
||||||
clauses = append(clauses, "performers."+col+" >= ?")
|
f.addWhere(col+" >= ?", startOfYear)
|
||||||
clauses = append(clauses, "performers."+col+" <= ?")
|
f.addWhere(col+" <= ?", endOfYear)
|
||||||
args = append(args, startOfYear)
|
case models.CriterionModifierNotEquals:
|
||||||
args = append(args, endOfYear)
|
|
||||||
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."+col+" < ? OR performers."+col+" > ?")
|
f.addWhere(col+" < ? OR "+col+" > ?", startOfYear, endOfYear)
|
||||||
args = append(args, startOfYear)
|
case models.CriterionModifierGreaterThan:
|
||||||
args = append(args, endOfYear)
|
|
||||||
case "GREATER_THAN":
|
|
||||||
// > yyyy-12-31
|
// > yyyy-12-31
|
||||||
clauses = append(clauses, "performers."+col+" > ?")
|
f.addWhere(col+" > ?", endOfYear)
|
||||||
args = append(args, endOfYear)
|
case models.CriterionModifierLessThan:
|
||||||
case "LESS_THAN":
|
|
||||||
// < yyyy-01-01
|
// < yyyy-01-01
|
||||||
clauses = append(clauses, "performers."+col+" < ?")
|
f.addWhere(col+" < ?", startOfYear)
|
||||||
args = append(args, startOfYear)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return clauses, args
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAgeFilterClause(criterionModifier models.CriterionModifier, value int) ([]string, []interface{}) {
|
func performerAgeFilterCriterionHandler(age *models.IntCriterionInput) criterionHandlerFunc {
|
||||||
var clauses []string
|
return func(f *filterBuilder) {
|
||||||
var args []interface{}
|
if age != nil && age.Modifier.IsValid() {
|
||||||
var clause string
|
var op string
|
||||||
|
|
||||||
if criterionModifier.IsValid() {
|
switch age.Modifier {
|
||||||
switch criterionModifier {
|
|
||||||
case models.CriterionModifierEquals:
|
case models.CriterionModifierEquals:
|
||||||
clause = " == ?"
|
op = "=="
|
||||||
case models.CriterionModifierNotEquals:
|
case models.CriterionModifierNotEquals:
|
||||||
clause = " != ?"
|
op = "!="
|
||||||
case models.CriterionModifierGreaterThan:
|
case models.CriterionModifierGreaterThan:
|
||||||
clause = " > ?"
|
op = ">"
|
||||||
case models.CriterionModifierLessThan:
|
case models.CriterionModifierLessThan:
|
||||||
clause = " < ?"
|
op = "<"
|
||||||
}
|
}
|
||||||
|
|
||||||
if clause != "" {
|
if op != "" {
|
||||||
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("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)
|
||||||
args = append(args, 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 clauses, args
|
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 {
|
||||||
|
|||||||
@@ -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: ðCriterion,
|
||||||
|
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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user