More performer filter criteria (#179)

* Add new performer filter criteria to UI

* Add backend support for new performer criteria
This commit is contained in:
WithoutPants
2019-11-07 15:36:48 +11:00
committed by Leopere
parent c0911f1626
commit 3c089dd97c
7 changed files with 478 additions and 43 deletions

View File

@@ -2,6 +2,8 @@ package models
import (
"database/sql"
"strconv"
"time"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
@@ -111,30 +113,60 @@ func (qb *PerformerQueryBuilder) Query(performerFilter *PerformerFilterType, fin
findFilter = &FindFilterType{}
}
var whereClauses []string
var havingClauses []string
var args []interface{}
body := selectDistinctIDs("performers")
body += `
query := queryBuilder{
tableName: "performers",
}
query.body = selectDistinctIDs("performers")
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
`
if q := findFilter.Q; q != nil && *q != "" {
searchColumns := []string{"performers.name", "performers.checksum", "performers.birthdate", "performers.ethnicity"}
whereClauses = append(whereClauses, getSearch(searchColumns, *q))
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
query.addWhere(clause)
query.addArg(thisArgs...)
}
if favoritesFilter := performerFilter.FilterFavorites; favoritesFilter != nil {
var favStr string
if *favoritesFilter == true {
whereClauses = append(whereClauses, "performers.favorite = 1")
favStr = "1"
} else {
whereClauses = append(whereClauses, "performers.favorite = 0")
favStr = "0"
}
query.addWhere("performers.favorite = " + favStr)
}
sortAndPagination := qb.getPerformerSort(findFilter) + getPagination(findFilter)
idsResult, countResult := executeFindQuery("performers", body, args, sortAndPagination, whereClauses, havingClauses)
if birthYear := performerFilter.BirthYear; birthYear != nil {
clauses, thisArgs := getBirthYearFilterClause(birthYear.Modifier, birthYear.Value)
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...)
}
handleStringCriterion("ethnicity", performerFilter.Ethnicity, &query)
handleStringCriterion("country", performerFilter.Country, &query)
handleStringCriterion("eye_color", performerFilter.EyeColor, &query)
handleStringCriterion("height", performerFilter.Height, &query)
handleStringCriterion("measurements", performerFilter.Measurements, &query)
handleStringCriterion("fake_tits", performerFilter.FakeTits, &query)
handleStringCriterion("career_length", performerFilter.CareerLength, &query)
handleStringCriterion("tattoos", performerFilter.Tattoos, &query)
handleStringCriterion("piercings", performerFilter.Piercings, &query)
// TODO - need better handling of aliases
handleStringCriterion("aliases", performerFilter.Aliases, &query)
query.sortAndPagination = qb.getPerformerSort(findFilter) + getPagination(findFilter)
idsResult, countResult := query.executeFind()
var performers []*Performer
for _, id := range idsResult {
@@ -145,6 +177,98 @@ func (qb *PerformerQueryBuilder) Query(performerFilter *PerformerFilterType, fin
return performers, countResult
}
func handleStringCriterion(column string, value *StringCriterionInput, query *queryBuilder) {
if value != nil {
if modifier := value.Modifier.String(); value.Modifier.IsValid() {
switch modifier {
case "EQUALS":
clause, thisArgs := getSearchBinding([]string{column}, value.Value, false)
query.addWhere(clause)
query.addArg(thisArgs...)
case "NOT_EQUALS":
clause, thisArgs := getSearchBinding([]string{column}, value.Value, true)
query.addWhere(clause)
query.addArg(thisArgs...)
case "IS_NULL":
query.addWhere(column + " IS NULL")
case "NOT_NULL":
query.addWhere(column + " IS NOT NULL")
}
}
}
}
func getBirthYearFilterClause(criterionModifier CriterionModifier, value int) ([]string, []interface{}) {
var clauses []string
var args []interface{}
yearStr := strconv.Itoa(value)
startOfYear := yearStr + "-01-01"
endOfYear := yearStr + "-12-31"
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
switch modifier {
case "EQUALS":
// between yyyy-01-01 and yyyy-12-31
clauses = append(clauses, "performers.birthdate >= ?")
clauses = append(clauses, "performers.birthdate <= ?")
args = append(args, startOfYear)
args = append(args, endOfYear)
case "NOT_EQUALS":
// outside of yyyy-01-01 to yyyy-12-31
clauses = append(clauses, "performers.birthdate < ? OR performers.birthdate > ?")
args = append(args, startOfYear)
args = append(args, endOfYear)
case "GREATER_THAN":
// > yyyy-12-31
clauses = append(clauses, "performers.birthdate > ?")
args = append(args, endOfYear)
case "LESS_THAN":
// < yyyy-01-01
clauses = append(clauses, "performers.birthdate < ?")
args = append(args, startOfYear)
}
}
return clauses, args
}
func getAgeFilterClause(criterionModifier CriterionModifier, value int) ([]string, []interface{}) {
var clauses []string
var args []interface{}
// get the date at which performer would turn the age specified
dt := time.Now()
birthDate := dt.AddDate(-value-1, 0, 0)
yearAfter := birthDate.AddDate(1, 0, 0)
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
switch modifier {
case "EQUALS":
// between birthDate and yearAfter
clauses = append(clauses, "performers.birthdate >= ?")
clauses = append(clauses, "performers.birthdate < ?")
args = append(args, birthDate)
args = append(args, yearAfter)
case "NOT_EQUALS":
// outside of birthDate and yearAfter
clauses = append(clauses, "performers.birthdate < ? OR performers.birthdate >= ?")
args = append(args, birthDate)
args = append(args, yearAfter)
case "GREATER_THAN":
// < birthDate
clauses = append(clauses, "performers.birthdate < ?")
args = append(args, birthDate)
case "LESS_THAN":
// > yearAfter
clauses = append(clauses, "performers.birthdate >= ?")
args = append(args, yearAfter)
}
}
return clauses, args
}
func (qb *PerformerQueryBuilder) getPerformerSort(findFilter *FindFilterType) string {
var sort string
var direction string

View File

@@ -13,6 +13,33 @@ import (
"github.com/stashapp/stash/pkg/logger"
)
type queryBuilder struct {
tableName string
body string
whereClauses []string
havingClauses []string
args []interface{}
sortAndPagination string
}
func (qb queryBuilder) executeFind() ([]int, int) {
return executeFindQuery(qb.tableName, qb.body, qb.args, qb.sortAndPagination, qb.whereClauses, qb.havingClauses)
}
func (qb *queryBuilder) addWhere(clauses ...string) {
qb.whereClauses = append(qb.whereClauses, clauses...)
}
func (qb *queryBuilder) addHaving(clauses ...string) {
qb.havingClauses = append(qb.havingClauses, clauses...)
}
func (qb *queryBuilder) addArg(args ...interface{}) {
qb.args = append(qb.args, args...)
}
var randomSortFloat = rand.Float64()
func selectAll(tableName string) string {
@@ -92,6 +119,7 @@ func getSort(sort string, direction string, tableName string) string {
}
func getSearch(columns []string, q string) string {
// TODO - susceptible to SQL injection
var likeClauses []string
queryWords := strings.Split(q, " ")
trimmedQuery := strings.Trim(q, "\"")
@@ -113,6 +141,39 @@ func getSearch(columns []string, q string) string {
return "(" + likes + ")"
}
func getSearchBinding(columns []string, q string, not bool) (string, []interface{}) {
var likeClauses []string
var args []interface{}
notStr := ""
binaryType := " OR "
if not {
notStr = " NOT "
binaryType = " AND "
}
queryWords := strings.Split(q, " ")
trimmedQuery := strings.Trim(q, "\"")
if trimmedQuery == q {
// Search for any word
for _, word := range queryWords {
for _, column := range columns {
likeClauses = append(likeClauses, column+notStr+" LIKE ?")
args = append(args, "%"+word+"%")
}
}
} else {
// Search the exact query
for _, column := range columns {
likeClauses = append(likeClauses, column+notStr+" LIKE ?")
args = append(args, "%"+trimmedQuery+"%")
}
}
likes := strings.Join(likeClauses, binaryType)
return "(" + likes + ")", args
}
func getInBinding(length int) string {
bindings := strings.Repeat("?, ", length)
bindings = strings.TrimRight(bindings, ", ")
@@ -128,22 +189,11 @@ func getCriterionModifierBinding(criterionModifier CriterionModifier, value inte
length = len(x)
default:
length = 1
logger.Debugf("unsupported type: %T\n", x)
}
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
switch modifier {
case "EQUALS":
return "= ?", 1
case "NOT_EQUALS":
return "!= ?", 1
case "GREATER_THAN":
return "> ?", 1
case "LESS_THAN":
return "< ?", 1
case "IS_NULL":
return "IS NULL", 0
case "NOT_NULL":
return "IS NOT NULL", 0
case "EQUALS", "NOT_EQUALS", "GREATER_THAN", "LESS_THAN", "IS_NULL", "NOT_NULL":
return getSimpleCriterionClause(criterionModifier, "?")
case "INCLUDES":
return "IN " + getInBinding(length), length // TODO?
case "EXCLUDES":
@@ -156,6 +206,30 @@ func getCriterionModifierBinding(criterionModifier CriterionModifier, value inte
return "= ?", 1 // TODO
}
func getSimpleCriterionClause(criterionModifier CriterionModifier, rhs string) (string, int) {
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
switch modifier {
case "EQUALS":
return "= " + rhs, 1
case "NOT_EQUALS":
return "!= " + rhs, 1
case "GREATER_THAN":
return "> " + rhs, 1
case "LESS_THAN":
return "< " + rhs, 1
case "IS_NULL":
return "IS NULL", 0
case "NOT_NULL":
return "IS NOT NULL", 0
default:
logger.Errorf("todo")
return "= ?", 1 // TODO
}
}
return "= ?", 1 // TODO
}
func getIntCriterionWhereClause(column string, input IntCriterionInput) (string, int) {
binding, count := getCriterionModifierBinding(input.Modifier, input.Value)
return column + " " + binding, count