mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
More performer filter criteria (#179)
* Add new performer filter criteria to UI * Add backend support for new performer criteria
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user