Join count filter criteria (#1254)

Co-authored-by: mrbrdo <mrbrdo@gmail.com>
Co-authored-by: peolic <66393006+peolic@users.noreply.github.com>
This commit is contained in:
WithoutPants
2021-04-09 18:46:00 +10:00
committed by GitHub
parent 6a0c73b3a1
commit a2582047ca
17 changed files with 743 additions and 29 deletions

View File

@@ -47,7 +47,7 @@ input PerformerFilterType {
measurements: StringCriterionInput measurements: StringCriterionInput
"""Filter by fake tits value""" """Filter by fake tits value"""
fake_tits: StringCriterionInput fake_tits: StringCriterionInput
"""Filter by career length""" """Filter by career length"""
career_length: StringCriterionInput career_length: StringCriterionInput
"""Filter by tattoos""" """Filter by tattoos"""
tattoos: StringCriterionInput tattoos: StringCriterionInput
@@ -61,6 +61,14 @@ input PerformerFilterType {
is_missing: String is_missing: String
"""Filter to only include performers with these tags""" """Filter to only include performers with these tags"""
tags: MultiCriterionInput tags: MultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter by scene count"""
scene_count: IntCriterionInput
"""Filter by image count"""
image_count: IntCriterionInput
"""Filter by gallery count"""
gallery_count: IntCriterionInput
"""Filter by StashID""" """Filter by StashID"""
stash_id: String stash_id: String
"""Filter by url""" """Filter by url"""
@@ -82,7 +90,7 @@ input SceneFilterType {
AND: SceneFilterType AND: SceneFilterType
OR: SceneFilterType OR: SceneFilterType
NOT: SceneFilterType NOT: SceneFilterType
"""Filter by path""" """Filter by path"""
path: StringCriterionInput path: StringCriterionInput
"""Filter by rating""" """Filter by rating"""
@@ -105,10 +113,14 @@ input SceneFilterType {
movies: MultiCriterionInput movies: MultiCriterionInput
"""Filter to only include scenes with these tags""" """Filter to only include scenes with these tags"""
tags: MultiCriterionInput tags: MultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter to only include scenes with performers with these tags""" """Filter to only include scenes with performers with these tags"""
performer_tags: MultiCriterionInput performer_tags: MultiCriterionInput
"""Filter to only include scenes with these performers""" """Filter to only include scenes with these performers"""
performers: MultiCriterionInput performers: MultiCriterionInput
"""Filter by performer count"""
performer_count: IntCriterionInput
"""Filter by StashID""" """Filter by StashID"""
stash_id: String stash_id: String
"""Filter by url""" """Filter by url"""
@@ -152,10 +164,14 @@ input GalleryFilterType {
studios: MultiCriterionInput studios: MultiCriterionInput
"""Filter to only include galleries with these tags""" """Filter to only include galleries with these tags"""
tags: MultiCriterionInput tags: MultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter to only include galleries with performers with these tags""" """Filter to only include galleries with performers with these tags"""
performer_tags: MultiCriterionInput performer_tags: MultiCriterionInput
"""Filter to only include galleries with these performers""" """Filter to only include galleries with these performers"""
performers: MultiCriterionInput performers: MultiCriterionInput
"""Filter by performer count"""
performer_count: IntCriterionInput
"""Filter by number of images in this gallery""" """Filter by number of images in this gallery"""
image_count: IntCriterionInput image_count: IntCriterionInput
"""Filter by url""" """Filter by url"""
@@ -203,10 +219,14 @@ input ImageFilterType {
studios: MultiCriterionInput studios: MultiCriterionInput
"""Filter to only include images with these tags""" """Filter to only include images with these tags"""
tags: MultiCriterionInput tags: MultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter to only include images with performers with these tags""" """Filter to only include images with performers with these tags"""
performer_tags: MultiCriterionInput performer_tags: MultiCriterionInput
"""Filter to only include images with these performers""" """Filter to only include images with these performers"""
performers: MultiCriterionInput performers: MultiCriterionInput
"""Filter by performer count"""
performer_count: IntCriterionInput
"""Filter to only include images with these galleries""" """Filter to only include images with these galleries"""
galleries: MultiCriterionInput galleries: MultiCriterionInput
} }

View File

@@ -405,3 +405,23 @@ func (m *multiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionI
} }
} }
} }
type countCriterionHandlerBuilder struct {
primaryTable string
joinTable string
primaryFK string
}
func (m *countCriterionHandlerBuilder) handler(criterion *models.IntCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) {
if criterion != nil {
clause, count := getCountCriterionClause(m.primaryTable, m.joinTable, m.primaryFK, *criterion)
if count == 1 {
f.addWhere(clause, criterion.Value)
} else {
f.addWhere(clause)
}
}
}
}

View File

@@ -239,6 +239,16 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi
query.addHaving(havingClause) query.addHaving(havingClause)
} }
if tagCountFilter := galleryFilter.TagCount; tagCountFilter != nil {
clause, count := getCountCriterionClause(galleryTable, galleriesTagsTable, galleryIDColumn, *tagCountFilter)
if count == 1 {
query.addArg(tagCountFilter.Value)
}
query.addWhere(clause)
}
if performersFilter := galleryFilter.Performers; performersFilter != nil && len(performersFilter.Value) > 0 { if performersFilter := galleryFilter.Performers; performersFilter != nil && len(performersFilter.Value) > 0 {
for _, performerID := range performersFilter.Value { for _, performerID := range performersFilter.Value {
query.addArg(performerID) query.addArg(performerID)
@@ -250,6 +260,16 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi
query.addHaving(havingClause) query.addHaving(havingClause)
} }
if performerCountFilter := galleryFilter.PerformerCount; performerCountFilter != nil {
clause, count := getCountCriterionClause(galleryTable, performersGalleriesTable, galleryIDColumn, *performerCountFilter)
if count == 1 {
query.addArg(performerCountFilter.Value)
}
query.addWhere(clause)
}
if studiosFilter := galleryFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 { if studiosFilter := galleryFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 {
for _, studioID := range studiosFilter.Value { for _, studioID := range studiosFilter.Value {
query.addArg(studioID) query.addArg(studioID)
@@ -382,7 +402,15 @@ func (qb *galleryQueryBuilder) getGallerySort(findFilter *models.FindFilterType)
sort = findFilter.GetSort("path") sort = findFilter.GetSort("path")
direction = findFilter.GetDirection() direction = findFilter.GetDirection()
} }
return getSort(sort, direction, "galleries")
switch sort {
case "tag_count":
return getCountSort(galleryTable, galleriesTagsTable, galleryIDColumn, direction)
case "performer_count":
return getCountSort(galleryTable, performersGalleriesTable, galleryIDColumn, direction)
default:
return getSort(sort, direction, "galleries")
}
} }
func (qb *galleryQueryBuilder) queryGallery(query string, args []interface{}) (*models.Gallery, error) { func (qb *galleryQueryBuilder) queryGallery(query string, args []interface{}) (*models.Gallery, error) {

View File

@@ -630,6 +630,88 @@ func TestGalleryQueryPerformerTags(t *testing.T) {
}) })
} }
func TestGalleryQueryTagCount(t *testing.T) {
const tagCount = 1
tagCountCriterion := models.IntCriterionInput{
Value: tagCount,
Modifier: models.CriterionModifierEquals,
}
verifyGalleriesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyGalleriesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyGalleriesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierLessThan
verifyGalleriesTagCount(t, tagCountCriterion)
}
func verifyGalleriesTagCount(t *testing.T, tagCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Gallery()
galleryFilter := models.GalleryFilterType{
TagCount: &tagCountCriterion,
}
galleries := queryGallery(t, sqb, &galleryFilter, nil)
assert.Greater(t, len(galleries), 0)
for _, gallery := range galleries {
ids, err := sqb.GetTagIDs(gallery.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), tagCountCriterion)
}
return nil
})
}
func TestGalleryQueryPerformerCount(t *testing.T) {
const performerCount = 1
performerCountCriterion := models.IntCriterionInput{
Value: performerCount,
Modifier: models.CriterionModifierEquals,
}
verifyGalleriesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyGalleriesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyGalleriesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierLessThan
verifyGalleriesPerformerCount(t, performerCountCriterion)
}
func verifyGalleriesPerformerCount(t *testing.T, performerCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Gallery()
galleryFilter := models.GalleryFilterType{
PerformerCount: &performerCountCriterion,
}
galleries := queryGallery(t, sqb, &galleryFilter, nil)
assert.Greater(t, len(galleries), 0)
for _, gallery := range galleries {
ids, err := sqb.GetPerformerIDs(gallery.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), performerCountCriterion)
}
return nil
})
}
// TODO Count // TODO Count
// TODO All // TODO All
// TODO Query // TODO Query

View File

@@ -328,6 +328,16 @@ func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilt
query.addHaving(havingClause) query.addHaving(havingClause)
} }
if tagCountFilter := imageFilter.TagCount; tagCountFilter != nil {
clause, count := getCountCriterionClause(imageTable, imagesTagsTable, imageIDColumn, *tagCountFilter)
if count == 1 {
query.addArg(tagCountFilter.Value)
}
query.addWhere(clause)
}
if galleriesFilter := imageFilter.Galleries; galleriesFilter != nil && len(galleriesFilter.Value) > 0 { if galleriesFilter := imageFilter.Galleries; galleriesFilter != nil && len(galleriesFilter.Value) > 0 {
for _, galleryID := range galleriesFilter.Value { for _, galleryID := range galleriesFilter.Value {
query.addArg(galleryID) query.addArg(galleryID)
@@ -350,6 +360,16 @@ func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilt
query.addHaving(havingClause) query.addHaving(havingClause)
} }
if performerCountFilter := imageFilter.PerformerCount; performerCountFilter != nil {
clause, count := getCountCriterionClause(imageTable, performersImagesTable, imageIDColumn, *performerCountFilter)
if count == 1 {
query.addArg(performerCountFilter.Value)
}
query.addWhere(clause)
}
if studiosFilter := imageFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 { if studiosFilter := imageFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 {
for _, studioID := range studiosFilter.Value { for _, studioID := range studiosFilter.Value {
query.addArg(studioID) query.addArg(studioID)
@@ -412,7 +432,15 @@ func (qb *imageQueryBuilder) getImageSort(findFilter *models.FindFilterType) str
} }
sort := findFilter.GetSort("title") sort := findFilter.GetSort("title")
direction := findFilter.GetDirection() direction := findFilter.GetDirection()
return getSort(sort, direction, "images")
switch sort {
case "tag_count":
return getCountSort(imageTable, imagesTagsTable, imageIDColumn, direction)
case "performer_count":
return getCountSort(imageTable, performersImagesTable, imageIDColumn, direction)
default:
return getSort(sort, direction, "images")
}
} }
func (qb *imageQueryBuilder) queryImage(query string, args []interface{}) (*models.Image, error) { func (qb *imageQueryBuilder) queryImage(query string, args []interface{}) (*models.Image, error) {

View File

@@ -683,6 +683,88 @@ func TestImageQueryPerformerTags(t *testing.T) {
}) })
} }
func TestImageQueryTagCount(t *testing.T) {
const tagCount = 1
tagCountCriterion := models.IntCriterionInput{
Value: tagCount,
Modifier: models.CriterionModifierEquals,
}
verifyImagesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyImagesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyImagesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierLessThan
verifyImagesTagCount(t, tagCountCriterion)
}
func verifyImagesTagCount(t *testing.T, tagCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Image()
imageFilter := models.ImageFilterType{
TagCount: &tagCountCriterion,
}
images := queryImages(t, sqb, &imageFilter, nil)
assert.Greater(t, len(images), 0)
for _, image := range images {
ids, err := sqb.GetTagIDs(image.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), tagCountCriterion)
}
return nil
})
}
func TestImageQueryPerformerCount(t *testing.T) {
const performerCount = 1
performerCountCriterion := models.IntCriterionInput{
Value: performerCount,
Modifier: models.CriterionModifierEquals,
}
verifyImagesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyImagesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyImagesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierLessThan
verifyImagesPerformerCount(t, performerCountCriterion)
}
func verifyImagesPerformerCount(t *testing.T, performerCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Image()
imageFilter := models.ImageFilterType{
PerformerCount: &performerCountCriterion,
}
images := queryImages(t, sqb, &imageFilter, nil)
assert.Greater(t, len(images), 0)
for _, image := range images {
ids, err := sqb.GetPerformerIDs(image.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), performerCountCriterion)
}
return nil
})
}
func TestImageQuerySorting(t *testing.T) { func TestImageQuerySorting(t *testing.T) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
sort := titleField sort := titleField

View File

@@ -271,6 +271,11 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy
query.addHaving(havingClause) 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()
if err != nil { if err != nil {
@@ -370,6 +375,11 @@ func (qb *performerQueryBuilder) getPerformerSort(findFilter *models.FindFilterT
sort = findFilter.GetSort("name") sort = findFilter.GetSort("name")
direction = findFilter.GetDirection() direction = findFilter.GetDirection()
} }
if sort == "tag_count" {
return getCountSort(performerTable, performersTagsTable, performerIDColumn, direction)
}
return getSort(sort, direction, "performers") return getSort(sort, direction, "performers")
} }

View File

@@ -387,6 +387,188 @@ func TestPerformerQueryTags(t *testing.T) {
}) })
} }
func TestPerformerQueryTagCount(t *testing.T) {
const tagCount = 1
tagCountCriterion := models.IntCriterionInput{
Value: tagCount,
Modifier: models.CriterionModifierEquals,
}
verifyPerformersTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyPerformersTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyPerformersTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierLessThan
verifyPerformersTagCount(t, tagCountCriterion)
}
func verifyPerformersTagCount(t *testing.T, tagCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Performer()
performerFilter := models.PerformerFilterType{
TagCount: &tagCountCriterion,
}
performers := queryPerformers(t, sqb, &performerFilter, nil)
assert.Greater(t, len(performers), 0)
for _, performer := range performers {
ids, err := sqb.GetTagIDs(performer.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), tagCountCriterion)
}
return nil
})
}
func TestPerformerQuerySceneCount(t *testing.T) {
const sceneCount = 1
sceneCountCriterion := models.IntCriterionInput{
Value: sceneCount,
Modifier: models.CriterionModifierEquals,
}
verifyPerformersSceneCount(t, sceneCountCriterion)
sceneCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyPerformersSceneCount(t, sceneCountCriterion)
sceneCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyPerformersSceneCount(t, sceneCountCriterion)
sceneCountCriterion.Modifier = models.CriterionModifierLessThan
verifyPerformersSceneCount(t, sceneCountCriterion)
}
func verifyPerformersSceneCount(t *testing.T, sceneCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Performer()
performerFilter := models.PerformerFilterType{
SceneCount: &sceneCountCriterion,
}
performers := queryPerformers(t, sqb, &performerFilter, nil)
assert.Greater(t, len(performers), 0)
for _, performer := range performers {
ids, err := r.Scene().FindByPerformerID(performer.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), sceneCountCriterion)
}
return nil
})
}
func TestPerformerQueryImageCount(t *testing.T) {
const imageCount = 1
imageCountCriterion := models.IntCriterionInput{
Value: imageCount,
Modifier: models.CriterionModifierEquals,
}
verifyPerformersImageCount(t, imageCountCriterion)
imageCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyPerformersImageCount(t, imageCountCriterion)
imageCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyPerformersImageCount(t, imageCountCriterion)
imageCountCriterion.Modifier = models.CriterionModifierLessThan
verifyPerformersImageCount(t, imageCountCriterion)
}
func verifyPerformersImageCount(t *testing.T, imageCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Performer()
performerFilter := models.PerformerFilterType{
ImageCount: &imageCountCriterion,
}
performers := queryPerformers(t, sqb, &performerFilter, nil)
assert.Greater(t, len(performers), 0)
for _, performer := range performers {
pp := 0
_, count, err := r.Image().Query(&models.ImageFilterType{
Performers: &models.MultiCriterionInput{
Value: []string{strconv.Itoa(performer.ID)},
Modifier: models.CriterionModifierIncludes,
},
}, &models.FindFilterType{
PerPage: &pp,
})
if err != nil {
return err
}
verifyInt(t, count, imageCountCriterion)
}
return nil
})
}
func TestPerformerQueryGalleryCount(t *testing.T) {
const galleryCount = 1
galleryCountCriterion := models.IntCriterionInput{
Value: galleryCount,
Modifier: models.CriterionModifierEquals,
}
verifyPerformersGalleryCount(t, galleryCountCriterion)
galleryCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyPerformersGalleryCount(t, galleryCountCriterion)
galleryCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyPerformersGalleryCount(t, galleryCountCriterion)
galleryCountCriterion.Modifier = models.CriterionModifierLessThan
verifyPerformersGalleryCount(t, galleryCountCriterion)
}
func verifyPerformersGalleryCount(t *testing.T, galleryCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Performer()
performerFilter := models.PerformerFilterType{
GalleryCount: &galleryCountCriterion,
}
performers := queryPerformers(t, sqb, &performerFilter, nil)
assert.Greater(t, len(performers), 0)
for _, performer := range performers {
pp := 0
_, count, err := r.Gallery().Query(&models.GalleryFilterType{
Performers: &models.MultiCriterionInput{
Value: []string{strconv.Itoa(performer.ID)},
Modifier: models.CriterionModifierIncludes,
},
}, &models.FindFilterType{
PerPage: &pp,
})
if err != nil {
return err
}
verifyInt(t, count, galleryCountCriterion)
}
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

@@ -151,3 +151,15 @@ func (qb *queryBuilder) handleStringCriterionInput(c *models.StringCriterionInpu
} }
} }
} }
func (qb *queryBuilder) handleCountCriterion(countFilter *models.IntCriterionInput, primaryTable, joinTable, primaryFK string) {
if countFilter != nil {
clause, count := getCountCriterionClause(primaryTable, joinTable, primaryFK, *countFilter)
if count == 1 {
qb.addArg(countFilter.Value)
}
qb.addWhere(clause)
}
}

View File

@@ -286,7 +286,7 @@ func (qb *sceneQueryBuilder) Wall(q *string) ([]*models.Scene, error) {
} }
func (qb *sceneQueryBuilder) All() ([]*models.Scene, error) { func (qb *sceneQueryBuilder) All() ([]*models.Scene, error) {
return qb.queryScenes(selectAll(sceneTable)+qb.getSceneSort(nil), nil) return qb.queryScenes(selectAll(sceneTable)+qb.getDefaultSceneSort(), nil)
} }
func illegalFilterCombination(type1, type2 string) error { func illegalFilterCombination(type1, type2 string) error {
@@ -348,7 +348,9 @@ func (qb *sceneQueryBuilder) makeFilter(sceneFilter *models.SceneFilterType) *fi
query.handleCriterionFunc(stringCriterionHandler(sceneFilter.URL, "scenes.url")) query.handleCriterionFunc(stringCriterionHandler(sceneFilter.URL, "scenes.url"))
query.handleCriterionFunc(sceneTagsCriterionHandler(qb, sceneFilter.Tags)) query.handleCriterionFunc(sceneTagsCriterionHandler(qb, sceneFilter.Tags))
query.handleCriterionFunc(sceneTagCountCriterionHandler(qb, sceneFilter.TagCount))
query.handleCriterionFunc(scenePerformersCriterionHandler(qb, sceneFilter.Performers)) query.handleCriterionFunc(scenePerformersCriterionHandler(qb, sceneFilter.Performers))
query.handleCriterionFunc(scenePerformerCountCriterionHandler(qb, sceneFilter.PerformerCount))
query.handleCriterionFunc(sceneStudioCriterionHandler(qb, sceneFilter.Studios)) query.handleCriterionFunc(sceneStudioCriterionHandler(qb, sceneFilter.Studios))
query.handleCriterionFunc(sceneMoviesCriterionHandler(qb, sceneFilter.Movies)) query.handleCriterionFunc(sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
query.handleCriterionFunc(sceneStashIDsHandler(qb, sceneFilter.StashID)) query.handleCriterionFunc(sceneStashIDsHandler(qb, sceneFilter.StashID))
@@ -384,7 +386,8 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt
query.addFilter(filter) query.addFilter(filter)
query.sortAndPagination = qb.getSceneSort(findFilter) + getPagination(findFilter) qb.setSceneSort(&query, findFilter)
query.sortAndPagination += getPagination(findFilter)
idsResult, countResult, err := query.executeFind() idsResult, countResult, err := query.executeFind()
if err != nil { if err != nil {
@@ -520,6 +523,7 @@ func (qb *sceneQueryBuilder) getMultiCriterionHandlerBuilder(foreignTable, joinT
addJoinsFunc: addJoinsFunc, addJoinsFunc: addJoinsFunc,
} }
} }
func sceneTagsCriterionHandler(qb *sceneQueryBuilder, tags *models.MultiCriterionInput) criterionHandlerFunc { func sceneTagsCriterionHandler(qb *sceneQueryBuilder, tags *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) { addJoinsFunc := func(f *filterBuilder) {
qb.tagsRepository().join(f, "tags_join", "scenes.id") qb.tagsRepository().join(f, "tags_join", "scenes.id")
@@ -530,6 +534,16 @@ func sceneTagsCriterionHandler(qb *sceneQueryBuilder, tags *models.MultiCriterio
return h.handler(tags) return h.handler(tags)
} }
func sceneTagCountCriterionHandler(qb *sceneQueryBuilder, tagCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: scenesTagsTable,
primaryFK: sceneIDColumn,
}
return h.handler(tagCount)
}
func scenePerformersCriterionHandler(qb *sceneQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc { func scenePerformersCriterionHandler(qb *sceneQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) { addJoinsFunc := func(f *filterBuilder) {
qb.performersRepository().join(f, "performers_join", "scenes.id") qb.performersRepository().join(f, "performers_join", "scenes.id")
@@ -540,6 +554,16 @@ func scenePerformersCriterionHandler(qb *sceneQueryBuilder, performers *models.M
return h.handler(performers) return h.handler(performers)
} }
func scenePerformerCountCriterionHandler(qb *sceneQueryBuilder, performerCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: performersScenesTable,
primaryFK: sceneIDColumn,
}
return h.handler(performerCount)
}
func sceneStudioCriterionHandler(qb *sceneQueryBuilder, studios *models.MultiCriterionInput) criterionHandlerFunc { func sceneStudioCriterionHandler(qb *sceneQueryBuilder, studios *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) { addJoinsFunc := func(f *filterBuilder) {
f.addJoin("studios", "studio", "studio.id = scenes.studio_id") f.addJoin("studios", "studio", "studio.id = scenes.studio_id")
@@ -586,8 +610,8 @@ func scenePerformerTagsCriterionHandler(qb *sceneQueryBuilder, performerTagsFilt
f.addWhere("performer_tags_join.tag_id IN "+getInBinding(len(performerTagsFilter.Value)), args...) f.addWhere("performer_tags_join.tag_id IN "+getInBinding(len(performerTagsFilter.Value)), args...)
f.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value))) f.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value)))
} else if performerTagsFilter.Modifier == models.CriterionModifierExcludes { } else if performerTagsFilter.Modifier == models.CriterionModifierExcludes {
f.addWhere(fmt.Sprintf(`not exists f.addWhere(fmt.Sprintf(`not exists
(select performers_scenes.performer_id from performers_scenes (select performers_scenes.performer_id from performers_scenes
left join performers_tags on performers_tags.performer_id = performers_scenes.performer_id where left join performers_tags on performers_tags.performer_id = performers_scenes.performer_id where
performers_scenes.scene_id = scenes.id AND performers_scenes.scene_id = scenes.id AND
performers_tags.tag_id in %s)`, getInBinding(len(performerTagsFilter.Value))), args...) performers_tags.tag_id in %s)`, getInBinding(len(performerTagsFilter.Value))), args...)
@@ -612,8 +636,8 @@ func handleScenePerformerTagsCriterion(query *queryBuilder, performerTagsFilter
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value))) query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
query.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value))) query.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value)))
} else if performerTagsFilter.Modifier == models.CriterionModifierExcludes { } else if performerTagsFilter.Modifier == models.CriterionModifierExcludes {
query.addWhere(fmt.Sprintf(`not exists query.addWhere(fmt.Sprintf(`not exists
(select performers_scenes.performer_id from performers_scenes (select performers_scenes.performer_id from performers_scenes
left join performers_tags on performers_tags.performer_id = performers_scenes.performer_id where left join performers_tags on performers_tags.performer_id = performers_scenes.performer_id where
performers_scenes.scene_id = scenes.id AND performers_scenes.scene_id = scenes.id AND
performers_tags.tag_id in %s)`, getInBinding(len(performerTagsFilter.Value)))) performers_tags.tag_id in %s)`, getInBinding(len(performerTagsFilter.Value))))
@@ -621,13 +645,25 @@ func handleScenePerformerTagsCriterion(query *queryBuilder, performerTagsFilter
} }
} }
func (qb *sceneQueryBuilder) getSceneSort(findFilter *models.FindFilterType) string { func (qb *sceneQueryBuilder) getDefaultSceneSort() string {
return " ORDER BY scenes.path, scenes.date ASC "
}
func (qb *sceneQueryBuilder) setSceneSort(query *queryBuilder, findFilter *models.FindFilterType) {
if findFilter == nil { if findFilter == nil {
return " ORDER BY scenes.path, scenes.date ASC " query.sortAndPagination += qb.getDefaultSceneSort()
return
} }
sort := findFilter.GetSort("title") sort := findFilter.GetSort("title")
direction := findFilter.GetDirection() direction := findFilter.GetDirection()
return getSort(sort, direction, "scenes") switch sort {
case "tag_count":
query.sortAndPagination += getCountSort(sceneTable, scenesTagsTable, sceneIDColumn, direction)
case "performer_count":
query.sortAndPagination += getCountSort(sceneTable, performersScenesTable, sceneIDColumn, direction)
default:
query.sortAndPagination += getSort(sort, direction, "scenes")
}
} }
func (qb *sceneQueryBuilder) queryScene(query string, args []interface{}) (*models.Scene, error) { func (qb *sceneQueryBuilder) queryScene(query string, args []interface{}) (*models.Scene, error) {

View File

@@ -1214,6 +1214,88 @@ func TestSceneQueryPagination(t *testing.T) {
}) })
} }
func TestSceneQueryTagCount(t *testing.T) {
const tagCount = 1
tagCountCriterion := models.IntCriterionInput{
Value: tagCount,
Modifier: models.CriterionModifierEquals,
}
verifyScenesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyScenesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyScenesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierLessThan
verifyScenesTagCount(t, tagCountCriterion)
}
func verifyScenesTagCount(t *testing.T, tagCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Scene()
sceneFilter := models.SceneFilterType{
TagCount: &tagCountCriterion,
}
scenes := queryScene(t, sqb, &sceneFilter, nil)
assert.Greater(t, len(scenes), 0)
for _, scene := range scenes {
ids, err := sqb.GetTagIDs(scene.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), tagCountCriterion)
}
return nil
})
}
func TestSceneQueryPerformerCount(t *testing.T) {
const performerCount = 1
performerCountCriterion := models.IntCriterionInput{
Value: performerCount,
Modifier: models.CriterionModifierEquals,
}
verifyScenesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyScenesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyScenesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierLessThan
verifyScenesPerformerCount(t, performerCountCriterion)
}
func verifyScenesPerformerCount(t *testing.T, performerCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Scene()
sceneFilter := models.SceneFilterType{
PerformerCount: &performerCountCriterion,
}
scenes := queryScene(t, sqb, &sceneFilter, nil)
assert.Greater(t, len(scenes), 0)
for _, scene := range scenes {
ids, err := sqb.GetPerformerIDs(scene.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), performerCountCriterion)
}
return nil
})
}
func TestSceneCountByTagID(t *testing.T) { func TestSceneCountByTagID(t *testing.T) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
sqb := r.Scene() sqb := r.Scene()

View File

@@ -24,6 +24,8 @@ const (
sceneIdxWithMovie = iota sceneIdxWithMovie = iota
sceneIdxWithGallery sceneIdxWithGallery
sceneIdxWithPerformer sceneIdxWithPerformer
sceneIdx1WithPerformer
sceneIdx2WithPerformer
sceneIdxWithTwoPerformers sceneIdxWithTwoPerformers
sceneIdxWithTag sceneIdxWithTag
sceneIdxWithTwoTags sceneIdxWithTwoTags
@@ -40,6 +42,8 @@ const (
const ( const (
imageIdxWithGallery = iota imageIdxWithGallery = iota
imageIdxWithPerformer imageIdxWithPerformer
imageIdx1WithPerformer
imageIdx2WithPerformer
imageIdxWithTwoPerformers imageIdxWithTwoPerformers
imageIdxWithTag imageIdxWithTag
imageIdxWithTwoTags imageIdxWithTwoTags
@@ -55,12 +59,15 @@ const (
performerIdxWithScene = iota performerIdxWithScene = iota
performerIdx1WithScene performerIdx1WithScene
performerIdx2WithScene performerIdx2WithScene
performerIdxWithTwoScenes
performerIdxWithImage performerIdxWithImage
performerIdxWithTwoImages
performerIdx1WithImage performerIdx1WithImage
performerIdx2WithImage performerIdx2WithImage
performerIdxWithTag performerIdxWithTag
performerIdxWithTwoTags performerIdxWithTwoTags
performerIdxWithGallery performerIdxWithGallery
performerIdxWithTwoGalleries
performerIdx1WithGallery performerIdx1WithGallery
performerIdx2WithGallery performerIdx2WithGallery
// new indexes above // new indexes above
@@ -87,6 +94,8 @@ const (
galleryIdxWithScene = iota galleryIdxWithScene = iota
galleryIdxWithImage galleryIdxWithImage
galleryIdxWithPerformer galleryIdxWithPerformer
galleryIdx1WithPerformer
galleryIdx2WithPerformer
galleryIdxWithTwoPerformers galleryIdxWithTwoPerformers
galleryIdxWithTag galleryIdxWithTag
galleryIdxWithTwoTags galleryIdxWithTwoTags
@@ -185,6 +194,8 @@ var (
{sceneIdxWithTwoPerformers, performerIdx2WithScene}, {sceneIdxWithTwoPerformers, performerIdx2WithScene},
{sceneIdxWithPerformerTag, performerIdxWithTag}, {sceneIdxWithPerformerTag, performerIdxWithTag},
{sceneIdxWithPerformerTwoTags, performerIdxWithTwoTags}, {sceneIdxWithPerformerTwoTags, performerIdxWithTwoTags},
{sceneIdx1WithPerformer, performerIdxWithTwoScenes},
{sceneIdx2WithPerformer, performerIdxWithTwoScenes},
} }
sceneGalleryLinks = [][2]int{ sceneGalleryLinks = [][2]int{
@@ -218,6 +229,8 @@ var (
{imageIdxWithTwoPerformers, performerIdx2WithImage}, {imageIdxWithTwoPerformers, performerIdx2WithImage},
{imageIdxWithPerformerTag, performerIdxWithTag}, {imageIdxWithPerformerTag, performerIdxWithTag},
{imageIdxWithPerformerTwoTags, performerIdxWithTwoTags}, {imageIdxWithPerformerTwoTags, performerIdxWithTwoTags},
{imageIdx1WithPerformer, performerIdxWithTwoImages},
{imageIdx2WithPerformer, performerIdxWithTwoImages},
} }
) )
@@ -228,6 +241,8 @@ var (
{galleryIdxWithTwoPerformers, performerIdx2WithGallery}, {galleryIdxWithTwoPerformers, performerIdx2WithGallery},
{galleryIdxWithPerformerTag, performerIdxWithTag}, {galleryIdxWithPerformerTag, performerIdxWithTag},
{galleryIdxWithPerformerTwoTags, performerIdxWithTwoTags}, {galleryIdxWithPerformerTwoTags, performerIdxWithTwoTags},
{galleryIdx1WithPerformer, performerIdxWithTwoGalleries},
{galleryIdx2WithPerformer, performerIdxWithTwoGalleries},
} }
galleryTagLinks = [][2]int{ galleryTagLinks = [][2]int{

View File

@@ -2,6 +2,7 @@ package sqlite
import ( import (
"database/sql" "database/sql"
"fmt"
"math/rand" "math/rand"
"strconv" "strconv"
"strings" "strings"
@@ -44,10 +45,15 @@ func getPaginationSQL(page int, perPage int) string {
return " LIMIT " + strconv.Itoa(perPage) + " OFFSET " + strconv.Itoa(page) + " " return " LIMIT " + strconv.Itoa(perPage) + " OFFSET " + strconv.Itoa(page) + " "
} }
func getSort(sort string, direction string, tableName string) string { func getSortDirection(direction string) string {
if direction != "ASC" && direction != "DESC" { if direction != "ASC" && direction != "DESC" {
direction = "ASC" return "ASC"
} else {
return direction
} }
}
func getSort(sort string, direction string, tableName string) string {
direction = getSortDirection(direction)
const randomSeedPrefix = "random_" const randomSeedPrefix = "random_"
@@ -96,6 +102,10 @@ func getRandomSort(tableName string, direction string, seed float64) string {
return " ORDER BY " + "(substr(" + colName + " * " + randomSortString + ", length(" + colName + ") + 2))" + " " + direction return " ORDER BY " + "(substr(" + colName + " * " + randomSortString + ", length(" + colName + ") + 2))" + " " + direction
} }
func getCountSort(primaryTable, joinTable, primaryFK, direction string) string {
return fmt.Sprintf(" ORDER BY (SELECT COUNT(*) FROM %s WHERE %s = %s.id) %s", joinTable, primaryFK, primaryTable, getSortDirection(direction))
}
func getSearchBinding(columns []string, q string, not bool) (string, []interface{}) { func getSearchBinding(columns []string, q string, not bool) (string, []interface{}) {
var likeClauses []string var likeClauses []string
var args []interface{} var args []interface{}
@@ -213,6 +223,11 @@ func getMultiCriterionClause(primaryTable, foreignTable, joinTable, primaryFK, f
return whereClause, havingClause return whereClause, havingClause
} }
func getCountCriterionClause(primaryTable, joinTable, primaryFK string, criterion models.IntCriterionInput) (string, int) {
lhs := fmt.Sprintf("(SELECT COUNT(*) FROM %s s WHERE s.%s = %s.id)", joinTable, primaryFK, primaryTable)
return getIntCriterionWhereClause(lhs, criterion)
}
func ensureTx(tx *sqlx.Tx) { func ensureTx(tx *sqlx.Tx) {
if tx == nil { if tx == nil {
panic("must use a transaction") panic("must use a transaction")

View File

@@ -3,6 +3,7 @@
* Added scene queue. * Added scene queue.
### 🎨 Improvements ### 🎨 Improvements
* Add various `count` filter criteria and sort options.
* Scroll to top when changing page number. * Scroll to top when changing page number.
* Add URL filter criteria for scenes, galleries, movies, performers and studios. * Add URL filter criteria for scenes, galleries, movies, performers and studios.
* Add HTTP endpoint for health checking at `/healthz`. * Add HTTP endpoint for health checking at `/healthz`.

View File

@@ -25,6 +25,7 @@ export type CriterionType =
| "tags" | "tags"
| "sceneTags" | "sceneTags"
| "performerTags" | "performerTags"
| "tag_count"
| "performers" | "performers"
| "studios" | "studios"
| "movies" | "movies"
@@ -90,6 +91,8 @@ export abstract class Criterion {
return "Scene Tags"; return "Scene Tags";
case "performerTags": case "performerTags":
return "Performer Tags"; return "Performer Tags";
case "tag_count":
return "Tag Count";
case "performers": case "performers":
return "Performers"; return "Performers";
case "studios": case "studios":
@@ -358,6 +361,15 @@ export class NumberCriterion extends Criterion {
} }
} }
export class MandatoryNumberCriterion extends NumberCriterion {
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals),
Criterion.getModifierOption(CriterionModifier.NotEquals),
Criterion.getModifierOption(CriterionModifier.GreaterThan),
Criterion.getModifierOption(CriterionModifier.LessThan),
];
}
export class DurationCriterion extends Criterion { export class DurationCriterion extends Criterion {
public type: CriterionType; public type: CriterionType;
public parameterName: string; public parameterName: string;

View File

@@ -1,12 +1,11 @@
/* eslint-disable consistent-return, default-case */ /* eslint-disable consistent-return, default-case */
import { CriterionModifier } from "src/core/generated-graphql";
import { import {
Criterion,
CriterionType, CriterionType,
StringCriterion, StringCriterion,
NumberCriterion, NumberCriterion,
DurationCriterion, DurationCriterion,
MandatoryStringCriterion, MandatoryStringCriterion,
MandatoryNumberCriterion,
} from "./criterion"; } from "./criterion";
import { OrganizedCriterion } from "./organized"; import { OrganizedCriterion } from "./organized";
import { FavoriteCriterion } from "./favorite"; import { FavoriteCriterion } from "./favorite";
@@ -46,7 +45,8 @@ export function makeCriteria(type: CriterionType = "none") {
case "image_count": case "image_count":
case "gallery_count": case "gallery_count":
case "performer_count": case "performer_count":
return new NumberCriterion(type, type); case "tag_count":
return new MandatoryNumberCriterion(type, type);
case "resolution": case "resolution":
return new ResolutionCriterion(); return new ResolutionCriterion();
case "average_resolution": case "average_resolution":
@@ -89,17 +89,8 @@ export function makeCriteria(type: CriterionType = "none") {
return new GalleriesCriterion(); return new GalleriesCriterion();
case "birth_year": case "birth_year":
return new NumberCriterion(type, type); return new NumberCriterion(type, type);
case "age": { case "age":
const ret = new NumberCriterion(type, type); return new MandatoryNumberCriterion(type, type);
// null/not null doesn't make sense for these criteria
ret.modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals),
Criterion.getModifierOption(CriterionModifier.NotEquals),
Criterion.getModifierOption(CriterionModifier.GreaterThan),
Criterion.getModifierOption(CriterionModifier.LessThan),
];
return ret;
}
case "gender": case "gender":
return new GenderCriterion(); return new GenderCriterion();
case "ethnicity": case "ethnicity":

View File

@@ -128,6 +128,8 @@ export class ListFilterModel {
"duration", "duration",
"framerate", "framerate",
"bitrate", "bitrate",
"tag_count",
"performer_count",
"random", "random",
]; ];
this.displayModeOptions = [ this.displayModeOptions = [
@@ -147,8 +149,10 @@ export class ListFilterModel {
new HasMarkersCriterionOption(), new HasMarkersCriterionOption(),
new SceneIsMissingCriterionOption(), new SceneIsMissingCriterionOption(),
new TagsCriterionOption(), new TagsCriterionOption(),
ListFilterModel.createCriterionOption("tag_count"),
new PerformerTagsCriterionOption(), new PerformerTagsCriterionOption(),
new PerformersCriterionOption(), new PerformersCriterionOption(),
ListFilterModel.createCriterionOption("performer_count"),
new StudiosCriterionOption(), new StudiosCriterionOption(),
new MoviesCriterionOption(), new MoviesCriterionOption(),
ListFilterModel.createCriterionOption("url"), ListFilterModel.createCriterionOption("url"),
@@ -163,6 +167,8 @@ export class ListFilterModel {
"o_counter", "o_counter",
"filesize", "filesize",
"file_mod_time", "file_mod_time",
"tag_count",
"performer_count",
"random", "random",
]; ];
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall]; this.displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
@@ -175,8 +181,10 @@ export class ListFilterModel {
new ResolutionCriterionOption(), new ResolutionCriterionOption(),
new ImageIsMissingCriterionOption(), new ImageIsMissingCriterionOption(),
new TagsCriterionOption(), new TagsCriterionOption(),
ListFilterModel.createCriterionOption("tag_count"),
new PerformerTagsCriterionOption(), new PerformerTagsCriterionOption(),
new PerformersCriterionOption(), new PerformersCriterionOption(),
ListFilterModel.createCriterionOption("performer_count"),
new StudiosCriterionOption(), new StudiosCriterionOption(),
]; ];
break; break;
@@ -187,6 +195,7 @@ export class ListFilterModel {
"height", "height",
"birthdate", "birthdate",
"scenes_count", "scenes_count",
"tag_count",
"random", "random",
]; ];
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List]; this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
@@ -212,6 +221,10 @@ export class ListFilterModel {
new PerformerIsMissingCriterionOption(), new PerformerIsMissingCriterionOption(),
new TagsCriterionOption(), new TagsCriterionOption(),
ListFilterModel.createCriterionOption("url"), ListFilterModel.createCriterionOption("url"),
ListFilterModel.createCriterionOption("tag_count"),
ListFilterModel.createCriterionOption("scene_count"),
ListFilterModel.createCriterionOption("image_count"),
ListFilterModel.createCriterionOption("gallery_count"),
...numberCriteria ...numberCriteria
.concat(stringCriteria) .concat(stringCriteria)
.map((c) => ListFilterModel.createCriterionOption(c)), .map((c) => ListFilterModel.createCriterionOption(c)),
@@ -247,6 +260,8 @@ export class ListFilterModel {
"path", "path",
"file_mod_time", "file_mod_time",
"images_count", "images_count",
"tag_count",
"performer_count",
"random", "random",
]; ];
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List]; this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
@@ -258,8 +273,10 @@ export class ListFilterModel {
new AverageResolutionCriterionOption(), new AverageResolutionCriterionOption(),
new GalleryIsMissingCriterionOption(), new GalleryIsMissingCriterionOption(),
new TagsCriterionOption(), new TagsCriterionOption(),
ListFilterModel.createCriterionOption("tag_count"),
new PerformerTagsCriterionOption(), new PerformerTagsCriterionOption(),
new PerformersCriterionOption(), new PerformersCriterionOption(),
ListFilterModel.createCriterionOption("performer_count"),
new StudiosCriterionOption(), new StudiosCriterionOption(),
ListFilterModel.createCriterionOption("url"), ListFilterModel.createCriterionOption("url"),
]; ];
@@ -563,6 +580,14 @@ export class ListFilterModel {
}; };
break; break;
} }
case "tag_count": {
const tagCountCrit = criterion as NumberCriterion;
result.tag_count = {
value: tagCountCrit.value,
modifier: tagCountCrit.modifier,
};
break;
}
case "performers": { case "performers": {
const perfCrit = criterion as PerformersCriterion; const perfCrit = criterion as PerformersCriterion;
result.performers = { result.performers = {
@@ -571,6 +596,14 @@ export class ListFilterModel {
}; };
break; break;
} }
case "performer_count": {
const performerCountCrit = criterion as NumberCriterion;
result.performer_count = {
value: performerCountCrit.value,
modifier: performerCountCrit.modifier,
};
break;
}
case "studios": { case "studios": {
const studCrit = criterion as StudiosCriterion; const studCrit = criterion as StudiosCriterion;
result.studios = { result.studios = {
@@ -711,9 +744,42 @@ export class ListFilterModel {
}; };
break; break;
} }
case "tag_count": {
const tagCountCrit = criterion as NumberCriterion;
result.tag_count = {
value: tagCountCrit.value,
modifier: tagCountCrit.modifier,
};
break;
}
case "scene_count": {
const countCrit = criterion as NumberCriterion;
result.scene_count = {
value: countCrit.value,
modifier: countCrit.modifier,
};
break;
}
case "image_count": {
const countCrit = criterion as NumberCriterion;
result.image_count = {
value: countCrit.value,
modifier: countCrit.modifier,
};
break;
}
case "gallery_count": {
const countCrit = criterion as NumberCriterion;
result.gallery_count = {
value: countCrit.value,
modifier: countCrit.modifier,
};
break;
}
// no default // no default
} }
}); });
return result; return result;
} }
@@ -839,6 +905,14 @@ export class ListFilterModel {
}; };
break; break;
} }
case "tag_count": {
const tagCountCrit = criterion as NumberCriterion;
result.tag_count = {
value: tagCountCrit.value,
modifier: tagCountCrit.modifier,
};
break;
}
case "performerTags": { case "performerTags": {
const performerTagsCrit = criterion as TagsCriterion; const performerTagsCrit = criterion as TagsCriterion;
result.performer_tags = { result.performer_tags = {
@@ -855,6 +929,14 @@ export class ListFilterModel {
}; };
break; break;
} }
case "performer_count": {
const countCrit = criterion as NumberCriterion;
result.performer_count = {
value: countCrit.value,
modifier: countCrit.modifier,
};
break;
}
case "studios": { case "studios": {
const studCrit = criterion as StudiosCriterion; const studCrit = criterion as StudiosCriterion;
result.studios = { result.studios = {
@@ -1014,6 +1096,14 @@ export class ListFilterModel {
}; };
break; break;
} }
case "tag_count": {
const tagCountCrit = criterion as NumberCriterion;
result.tag_count = {
value: tagCountCrit.value,
modifier: tagCountCrit.modifier,
};
break;
}
case "performerTags": { case "performerTags": {
const performerTagsCrit = criterion as TagsCriterion; const performerTagsCrit = criterion as TagsCriterion;
result.performer_tags = { result.performer_tags = {
@@ -1030,6 +1120,14 @@ export class ListFilterModel {
}; };
break; break;
} }
case "performer_count": {
const countCrit = criterion as NumberCriterion;
result.performer_count = {
value: countCrit.value,
modifier: countCrit.modifier,
};
break;
}
case "studios": { case "studios": {
const studCrit = criterion as StudiosCriterion; const studCrit = criterion as StudiosCriterion;
result.studios = { result.studios = {