mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Add Several Media Performer Detail Filters, Scene Filters and Sort (#2257)
Co-authored-by: bnkai <48220860+bnkai@users.noreply.github.com> Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
1d68492a5b
commit
4dd0bbc294
@@ -33,6 +33,12 @@ input ResolutionCriterionInput {
|
|||||||
modifier: CriterionModifier!
|
modifier: CriterionModifier!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input PHashDuplicationCriterionInput {
|
||||||
|
duplicated: Boolean
|
||||||
|
"""Currently unimplemented"""
|
||||||
|
distance: Int
|
||||||
|
}
|
||||||
|
|
||||||
input PerformerFilterType {
|
input PerformerFilterType {
|
||||||
AND: PerformerFilterType
|
AND: PerformerFilterType
|
||||||
OR: PerformerFilterType
|
OR: PerformerFilterType
|
||||||
@@ -130,6 +136,8 @@ input SceneFilterType {
|
|||||||
organized: Boolean
|
organized: Boolean
|
||||||
"""Filter by o-counter"""
|
"""Filter by o-counter"""
|
||||||
o_counter: IntCriterionInput
|
o_counter: IntCriterionInput
|
||||||
|
"""Filter Scenes that have an exact phash match available"""
|
||||||
|
duplicated: PHashDuplicationCriterionInput
|
||||||
"""Filter by resolution"""
|
"""Filter by resolution"""
|
||||||
resolution: ResolutionCriterionInput
|
resolution: ResolutionCriterionInput
|
||||||
"""Filter by duration (in seconds)"""
|
"""Filter by duration (in seconds)"""
|
||||||
@@ -148,6 +156,10 @@ input SceneFilterType {
|
|||||||
tag_count: IntCriterionInput
|
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: HierarchicalMultiCriterionInput
|
performer_tags: HierarchicalMultiCriterionInput
|
||||||
|
"""Filter scenes that have performers that have been favorited"""
|
||||||
|
performer_favorite: Boolean
|
||||||
|
"""Filter scenes by performer age at time of scene"""
|
||||||
|
performer_age: IntCriterionInput
|
||||||
"""Filter to only include scenes with these performers"""
|
"""Filter to only include scenes with these performers"""
|
||||||
performers: MultiCriterionInput
|
performers: MultiCriterionInput
|
||||||
"""Filter by performer count"""
|
"""Filter by performer count"""
|
||||||
@@ -243,6 +255,10 @@ input GalleryFilterType {
|
|||||||
performers: MultiCriterionInput
|
performers: MultiCriterionInput
|
||||||
"""Filter by performer count"""
|
"""Filter by performer count"""
|
||||||
performer_count: IntCriterionInput
|
performer_count: IntCriterionInput
|
||||||
|
"""Filter galleries that have performers that have been favorited"""
|
||||||
|
performer_favorite: Boolean
|
||||||
|
"""Filter galleries by performer age at time of gallery"""
|
||||||
|
performer_age: 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"""
|
||||||
@@ -324,6 +340,8 @@ input ImageFilterType {
|
|||||||
performers: MultiCriterionInput
|
performers: MultiCriterionInput
|
||||||
"""Filter by performer count"""
|
"""Filter by performer count"""
|
||||||
performer_count: IntCriterionInput
|
performer_count: IntCriterionInput
|
||||||
|
"""Filter images that have performers that have been favorited"""
|
||||||
|
performer_favorite: Boolean
|
||||||
"""Filter to only include images with these galleries"""
|
"""Filter to only include images with these galleries"""
|
||||||
galleries: MultiCriterionInput
|
galleries: MultiCriterionInput
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -391,13 +391,7 @@ func stringCriterionHandler(c *models.StringCriterionInput, column string) crite
|
|||||||
case models.CriterionModifierNotNull:
|
case models.CriterionModifierNotNull:
|
||||||
f.addWhere("(" + column + " IS NOT NULL AND TRIM(" + column + ") != '')")
|
f.addWhere("(" + column + " IS NOT NULL AND TRIM(" + column + ") != '')")
|
||||||
default:
|
default:
|
||||||
clause, count := getSimpleCriterionClause(modifier, "?")
|
panic("unsupported string filter modifier")
|
||||||
|
|
||||||
if count == 1 {
|
|
||||||
f.addWhere(column+" "+clause, c.Value)
|
|
||||||
} else {
|
|
||||||
f.addWhere(column + " " + clause)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,6 +220,8 @@ func (qb *galleryQueryBuilder) makeFilter(galleryFilter *models.GalleryFilterTyp
|
|||||||
query.handleCriterion(galleryPerformerTagsCriterionHandler(qb, galleryFilter.PerformerTags))
|
query.handleCriterion(galleryPerformerTagsCriterionHandler(qb, galleryFilter.PerformerTags))
|
||||||
query.handleCriterion(galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution))
|
query.handleCriterion(galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution))
|
||||||
query.handleCriterion(galleryImageCountCriterionHandler(qb, galleryFilter.ImageCount))
|
query.handleCriterion(galleryImageCountCriterionHandler(qb, galleryFilter.ImageCount))
|
||||||
|
query.handleCriterion(galleryPerformerFavoriteCriterionHandler(galleryFilter.PerformerFavorite))
|
||||||
|
query.handleCriterion(galleryPerformerAgeCriterionHandler(galleryFilter.PerformerAge))
|
||||||
|
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
@@ -421,6 +423,43 @@ INNER JOIN (` + valuesClause + `) t ON t.column2 = pt.tag_id
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func galleryPerformerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
|
||||||
|
return func(f *filterBuilder) {
|
||||||
|
if performerfavorite != nil {
|
||||||
|
f.addLeftJoin("performers_galleries", "", "galleries.id = performers_galleries.gallery_id")
|
||||||
|
|
||||||
|
if *performerfavorite {
|
||||||
|
// contains at least one favorite
|
||||||
|
f.addLeftJoin("performers", "", "performers.id = performers_galleries.performer_id")
|
||||||
|
f.addWhere("performers.favorite = 1")
|
||||||
|
} else {
|
||||||
|
// contains zero favorites
|
||||||
|
f.addLeftJoin(`(SELECT performers_galleries.gallery_id as id FROM performers_galleries
|
||||||
|
JOIN performers ON performers.id = performers_galleries.performer_id
|
||||||
|
GROUP BY performers_galleries.gallery_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "galleries.id = nofaves.id")
|
||||||
|
f.addWhere("performers_galleries.gallery_id IS NULL OR nofaves.id IS NOT NULL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryPerformerAgeCriterionHandler(performerAge *models.IntCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(f *filterBuilder) {
|
||||||
|
if performerAge != nil {
|
||||||
|
f.addInnerJoin("performers_galleries", "", "galleries.id = performers_galleries.gallery_id")
|
||||||
|
f.addInnerJoin("performers", "", "performers_galleries.performer_id = performers.id")
|
||||||
|
|
||||||
|
f.addWhere("galleries.date != '' AND performers.birthdate != ''")
|
||||||
|
f.addWhere("galleries.date IS NOT NULL AND performers.birthdate IS NOT NULL")
|
||||||
|
f.addWhere("galleries.date != '0001-01-01' AND performers.birthdate != '0001-01-01'")
|
||||||
|
|
||||||
|
ageCalc := "cast(strftime('%Y.%m%d', galleries.date) - strftime('%Y.%m%d', performers.birthdate) as int)"
|
||||||
|
whereClause, args := getIntWhereClause(ageCalc, performerAge.Modifier, performerAge.Value, performerAge.Value2)
|
||||||
|
f.addWhere(whereClause, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func galleryAverageResolutionCriterionHandler(qb *galleryQueryBuilder, resolution *models.ResolutionCriterionInput) criterionHandlerFunc {
|
func galleryAverageResolutionCriterionHandler(qb *galleryQueryBuilder, resolution *models.ResolutionCriterionInput) criterionHandlerFunc {
|
||||||
return func(f *filterBuilder) {
|
return func(f *filterBuilder) {
|
||||||
if resolution != nil && resolution.Value.IsValid() {
|
if resolution != nil && resolution.Value.IsValid() {
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ func (qb *imageQueryBuilder) makeFilter(imageFilter *models.ImageFilterType) *fi
|
|||||||
query.handleCriterion(imagePerformerCountCriterionHandler(qb, imageFilter.PerformerCount))
|
query.handleCriterion(imagePerformerCountCriterionHandler(qb, imageFilter.PerformerCount))
|
||||||
query.handleCriterion(imageStudioCriterionHandler(qb, imageFilter.Studios))
|
query.handleCriterion(imageStudioCriterionHandler(qb, imageFilter.Studios))
|
||||||
query.handleCriterion(imagePerformerTagsCriterionHandler(qb, imageFilter.PerformerTags))
|
query.handleCriterion(imagePerformerTagsCriterionHandler(qb, imageFilter.PerformerTags))
|
||||||
|
query.handleCriterion(imagePerformerFavoriteCriterionHandler(imageFilter.PerformerFavorite))
|
||||||
|
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
@@ -446,6 +447,26 @@ func imagePerformerCountCriterionHandler(qb *imageQueryBuilder, performerCount *
|
|||||||
return h.handler(performerCount)
|
return h.handler(performerCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func imagePerformerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
|
||||||
|
return func(f *filterBuilder) {
|
||||||
|
if performerfavorite != nil {
|
||||||
|
f.addLeftJoin("performers_images", "", "images.id = performers_images.image_id")
|
||||||
|
|
||||||
|
if *performerfavorite {
|
||||||
|
// contains at least one favorite
|
||||||
|
f.addLeftJoin("performers", "", "performers.id = performers_images.performer_id")
|
||||||
|
f.addWhere("performers.favorite = 1")
|
||||||
|
} else {
|
||||||
|
// contains zero favorites
|
||||||
|
f.addLeftJoin(`(SELECT performers_images.image_id as id FROM performers_images
|
||||||
|
JOIN performers ON performers.id = performers_images.performer_id
|
||||||
|
GROUP BY performers_images.image_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "images.id = nofaves.id")
|
||||||
|
f.addWhere("performers_images.image_id IS NULL OR nofaves.id IS NOT NULL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func imageStudioCriterionHandler(qb *imageQueryBuilder, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
func imageStudioCriterionHandler(qb *imageQueryBuilder, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||||
h := hierarchicalMultiCriterionHandlerBuilder{
|
h := hierarchicalMultiCriterionHandlerBuilder{
|
||||||
tx: qb.tx,
|
tx: qb.tx,
|
||||||
|
|||||||
@@ -392,6 +392,9 @@ func (qb *sceneQueryBuilder) makeFilter(sceneFilter *models.SceneFilterType) *fi
|
|||||||
query.handleCriterion(sceneStudioCriterionHandler(qb, sceneFilter.Studios))
|
query.handleCriterion(sceneStudioCriterionHandler(qb, sceneFilter.Studios))
|
||||||
query.handleCriterion(sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
|
query.handleCriterion(sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
|
||||||
query.handleCriterion(scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags))
|
query.handleCriterion(scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags))
|
||||||
|
query.handleCriterion(scenePerformerFavoriteCriterionHandler(sceneFilter.PerformerFavorite))
|
||||||
|
query.handleCriterion(scenePerformerAgeCriterionHandler(sceneFilter.PerformerAge))
|
||||||
|
query.handleCriterion(scenePhashDuplicatedCriterionHandler(sceneFilter.Duplicated))
|
||||||
|
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
@@ -504,6 +507,21 @@ func phashCriterionHandler(phashFilter *models.StringCriterionInput) criterionHa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scenePhashDuplicatedCriterionHandler(duplicatedFilter *models.PHashDuplicationCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(f *filterBuilder) {
|
||||||
|
// TODO: Wishlist item: Implement Distance matching
|
||||||
|
if duplicatedFilter != nil {
|
||||||
|
var v string
|
||||||
|
if *duplicatedFilter.Duplicated {
|
||||||
|
v = ">"
|
||||||
|
} else {
|
||||||
|
v = "="
|
||||||
|
}
|
||||||
|
f.addInnerJoin("(SELECT id FROM scenes JOIN (SELECT phash FROM scenes GROUP BY phash HAVING COUNT(phash) "+v+" 1) dupes on scenes.phash = dupes.phash)", "scph", "scenes.id = scph.id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func durationCriterionHandler(durationFilter *models.IntCriterionInput, column string) criterionHandlerFunc {
|
func durationCriterionHandler(durationFilter *models.IntCriterionInput, column string) criterionHandlerFunc {
|
||||||
return func(f *filterBuilder) {
|
return func(f *filterBuilder) {
|
||||||
if durationFilter != nil {
|
if durationFilter != nil {
|
||||||
@@ -642,6 +660,43 @@ func scenePerformerCountCriterionHandler(qb *sceneQueryBuilder, performerCount *
|
|||||||
return h.handler(performerCount)
|
return h.handler(performerCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scenePerformerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
|
||||||
|
return func(f *filterBuilder) {
|
||||||
|
if performerfavorite != nil {
|
||||||
|
f.addLeftJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id")
|
||||||
|
|
||||||
|
if *performerfavorite {
|
||||||
|
// contains at least one favorite
|
||||||
|
f.addLeftJoin("performers", "", "performers.id = performers_scenes.performer_id")
|
||||||
|
f.addWhere("performers.favorite = 1")
|
||||||
|
} else {
|
||||||
|
// contains zero favorites
|
||||||
|
f.addLeftJoin(`(SELECT performers_scenes.scene_id as id FROM performers_scenes
|
||||||
|
JOIN performers ON performers.id = performers_scenes.performer_id
|
||||||
|
GROUP BY performers_scenes.scene_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "scenes.id = nofaves.id")
|
||||||
|
f.addWhere("performers_scenes.scene_id IS NULL OR nofaves.id IS NOT NULL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scenePerformerAgeCriterionHandler(performerAge *models.IntCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(f *filterBuilder) {
|
||||||
|
if performerAge != nil {
|
||||||
|
f.addInnerJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id")
|
||||||
|
f.addInnerJoin("performers", "", "performers_scenes.performer_id = performers.id")
|
||||||
|
|
||||||
|
f.addWhere("scenes.date != '' AND performers.birthdate != ''")
|
||||||
|
f.addWhere("scenes.date IS NOT NULL AND performers.birthdate IS NOT NULL")
|
||||||
|
f.addWhere("scenes.date != '0001-01-01' AND performers.birthdate != '0001-01-01'")
|
||||||
|
|
||||||
|
ageCalc := "cast(strftime('%Y.%m%d', scenes.date) - strftime('%Y.%m%d', performers.birthdate) as int)"
|
||||||
|
whereClause, args := getIntWhereClause(ageCalc, performerAge.Modifier, performerAge.Value, performerAge.Value2)
|
||||||
|
f.addWhere(whereClause, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sceneStudioCriterionHandler(qb *sceneQueryBuilder, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
func sceneStudioCriterionHandler(qb *sceneQueryBuilder, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||||
h := hierarchicalMultiCriterionHandlerBuilder{
|
h := hierarchicalMultiCriterionHandlerBuilder{
|
||||||
tx: qb.tx,
|
tx: qb.tx,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,6 +65,10 @@ func getSort(sort string, direction string, tableName string) string {
|
|||||||
case strings.Compare(sort, "filesize") == 0:
|
case strings.Compare(sort, "filesize") == 0:
|
||||||
colName := getColumn(tableName, "size")
|
colName := getColumn(tableName, "size")
|
||||||
return " ORDER BY cast(" + colName + " as integer) " + direction
|
return " ORDER BY cast(" + colName + " as integer) " + direction
|
||||||
|
case strings.Compare(sort, "perceptual_similarity") == 0:
|
||||||
|
colName := getColumn(tableName, "phash")
|
||||||
|
secondaryColName := getColumn(tableName, "size")
|
||||||
|
return " ORDER BY " + colName + " " + direction + ", " + secondaryColName + " DESC"
|
||||||
case strings.HasPrefix(sort, randomSeedPrefix):
|
case strings.HasPrefix(sort, randomSeedPrefix):
|
||||||
// seed as a parameter from the UI
|
// seed as a parameter from the UI
|
||||||
// turn the provided seed into a float
|
// turn the provided seed into a float
|
||||||
@@ -149,54 +152,39 @@ func getInBinding(length int) string {
|
|||||||
return "(" + bindings + ")"
|
return "(" + bindings + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSimpleCriterionClause(criterionModifier models.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
|
|
||||||
case "BETWEEN":
|
|
||||||
return "BETWEEN (" + rhs + ") AND (" + rhs + ")", 2
|
|
||||||
case "NOT_BETWEEN":
|
|
||||||
return "NOT BETWEEN (" + rhs + ") AND (" + rhs + ")", 2
|
|
||||||
default:
|
|
||||||
logger.Errorf("todo")
|
|
||||||
return "= ?", 1 // TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "= ?", 1 // TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIntCriterionWhereClause(column string, input models.IntCriterionInput) (string, []interface{}) {
|
func getIntCriterionWhereClause(column string, input models.IntCriterionInput) (string, []interface{}) {
|
||||||
binding, _ := getSimpleCriterionClause(input.Modifier, "?")
|
return getIntWhereClause(column, input.Modifier, input.Value, input.Value2)
|
||||||
var args []interface{}
|
|
||||||
|
|
||||||
switch input.Modifier {
|
|
||||||
case "EQUALS", "NOT_EQUALS":
|
|
||||||
args = []interface{}{input.Value}
|
|
||||||
case "LESS_THAN":
|
|
||||||
args = []interface{}{input.Value}
|
|
||||||
case "GREATER_THAN":
|
|
||||||
args = []interface{}{input.Value}
|
|
||||||
case "BETWEEN", "NOT_BETWEEN":
|
|
||||||
upper := 0
|
|
||||||
if input.Value2 != nil {
|
|
||||||
upper = *input.Value2
|
|
||||||
}
|
|
||||||
args = []interface{}{input.Value, upper}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return column + " " + binding, args
|
func getIntWhereClause(column string, modifier models.CriterionModifier, value int, upper *int) (string, []interface{}) {
|
||||||
|
if upper == nil {
|
||||||
|
u := 0
|
||||||
|
upper = &u
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []interface{}{value}
|
||||||
|
betweenArgs := []interface{}{value, *upper}
|
||||||
|
|
||||||
|
switch modifier {
|
||||||
|
case models.CriterionModifierIsNull:
|
||||||
|
return fmt.Sprintf("%s IS NULL", column), nil
|
||||||
|
case models.CriterionModifierNotNull:
|
||||||
|
return fmt.Sprintf("%s IS NOT NULL", column), nil
|
||||||
|
case models.CriterionModifierEquals:
|
||||||
|
return fmt.Sprintf("%s = ?", column), args
|
||||||
|
case models.CriterionModifierNotEquals:
|
||||||
|
return fmt.Sprintf("%s != ?", column), args
|
||||||
|
case models.CriterionModifierBetween:
|
||||||
|
return fmt.Sprintf("%s BETWEEN ? AND ?", column), betweenArgs
|
||||||
|
case models.CriterionModifierNotBetween:
|
||||||
|
return fmt.Sprintf("%s NOT BETWEEN ? AND ?", column), betweenArgs
|
||||||
|
case models.CriterionModifierLessThan:
|
||||||
|
return fmt.Sprintf("%s < ?", column), args
|
||||||
|
case models.CriterionModifierGreaterThan:
|
||||||
|
return fmt.Sprintf("%s > ?", column), args
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("unsupported int modifier type")
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns where clause and having clause
|
// returns where clause and having clause
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Added support for filtering scenes, images and galleries featuring favourite performers and performer age at time of production. ([#2257](https://github.com/stashapp/stash/pull/2257))
|
||||||
|
* Added support for filtering scenes with (or without) phash duplicates. ([#2257](https://github.com/stashapp/stash/pull/2257))
|
||||||
|
* Added support for sorting scenes by phash. ([#2257](https://github.com/stashapp/stash/pull/2257))
|
||||||
* Open stash in system tray on Windows/MacOS when not running via terminal. ([#2073](https://github.com/stashapp/stash/pull/2073))
|
* Open stash in system tray on Windows/MacOS when not running via terminal. ([#2073](https://github.com/stashapp/stash/pull/2073))
|
||||||
* Optionally send desktop notifications when a task completes. ([#2073](https://github.com/stashapp/stash/pull/2073))
|
* Optionally send desktop notifications when a task completes. ([#2073](https://github.com/stashapp/stash/pull/2073))
|
||||||
* Added button to image card to view image in Lightbox. ([#2275](https://github.com/stashapp/stash/pull/2275))
|
* Added button to image card to view image in Lightbox. ([#2275](https://github.com/stashapp/stash/pull/2275))
|
||||||
|
|||||||
@@ -653,6 +653,7 @@
|
|||||||
"search_accuracy_label": "Search Accuracy",
|
"search_accuracy_label": "Search Accuracy",
|
||||||
"title": "Duplicate Scenes"
|
"title": "Duplicate Scenes"
|
||||||
},
|
},
|
||||||
|
"duplicated_phash": "Duplicated (phash)",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"effect_filters": {
|
"effect_filters": {
|
||||||
"aspect": "Aspect",
|
"aspect": "Aspect",
|
||||||
@@ -760,9 +761,12 @@
|
|||||||
"parent_tags": "Parent Tags",
|
"parent_tags": "Parent Tags",
|
||||||
"part_of": "Part of {parent}",
|
"part_of": "Part of {parent}",
|
||||||
"path": "Path",
|
"path": "Path",
|
||||||
|
"perceptual_similarity": "Perceptual Similarity (phash)",
|
||||||
"performer": "Performer",
|
"performer": "Performer",
|
||||||
"performerTags": "Performer Tags",
|
"performerTags": "Performer Tags",
|
||||||
"performer_count": "Performer Count",
|
"performer_count": "Performer Count",
|
||||||
|
"performer_favorite": "Performer Favourited",
|
||||||
|
"performer_age": "Performer Age",
|
||||||
"performer_image": "Performer Image",
|
"performer_image": "Performer Image",
|
||||||
"performers": "Performers",
|
"performers": "Performers",
|
||||||
"piercings": "Piercings",
|
"piercings": "Piercings",
|
||||||
|
|||||||
@@ -9,5 +9,6 @@
|
|||||||
"ignore_organized": "Ignore organized scenes"
|
"ignore_organized": "Ignore organized scenes"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"performer_favorite": "Performer Favorited"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
HierarchicalMultiCriterionInput,
|
HierarchicalMultiCriterionInput,
|
||||||
IntCriterionInput,
|
IntCriterionInput,
|
||||||
MultiCriterionInput,
|
MultiCriterionInput,
|
||||||
|
PHashDuplicationCriterionInput,
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import DurationUtils from "src/utils/duration";
|
import DurationUtils from "src/utils/duration";
|
||||||
import {
|
import {
|
||||||
@@ -521,3 +522,11 @@ export class DurationCriterion extends Criterion<INumberValue> {
|
|||||||
: "?";
|
: "?";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PhashDuplicateCriterion extends StringCriterion {
|
||||||
|
protected toCriterionInput(): PHashDuplicationCriterionInput {
|
||||||
|
return {
|
||||||
|
duplicated: this.value === "true",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
ILabeledIdCriterion,
|
ILabeledIdCriterion,
|
||||||
} from "./criterion";
|
} from "./criterion";
|
||||||
import { OrganizedCriterion } from "./organized";
|
import { OrganizedCriterion } from "./organized";
|
||||||
import { FavoriteCriterion } from "./favorite";
|
import { FavoriteCriterion, PerformerFavoriteCriterion } from "./favorite";
|
||||||
import { HasMarkersCriterion } from "./has-markers";
|
import { HasMarkersCriterion } from "./has-markers";
|
||||||
import {
|
import {
|
||||||
PerformerIsMissingCriterionOption,
|
PerformerIsMissingCriterionOption,
|
||||||
@@ -40,7 +40,7 @@ import { GalleriesCriterion } from "./galleries";
|
|||||||
import { CriterionType } from "../types";
|
import { CriterionType } from "../types";
|
||||||
import { InteractiveCriterion } from "./interactive";
|
import { InteractiveCriterion } from "./interactive";
|
||||||
import { RatingCriterionOption } from "./rating";
|
import { RatingCriterionOption } from "./rating";
|
||||||
import { PhashCriterionOption } from "./phash";
|
import { DuplicatedCriterion, PhashCriterionOption } from "./phash";
|
||||||
|
|
||||||
export function makeCriteria(type: CriterionType = "none") {
|
export function makeCriteria(type: CriterionType = "none") {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -67,6 +67,7 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
case "image_count":
|
case "image_count":
|
||||||
case "gallery_count":
|
case "gallery_count":
|
||||||
case "performer_count":
|
case "performer_count":
|
||||||
|
case "performer_age":
|
||||||
case "tag_count":
|
case "tag_count":
|
||||||
return new NumberCriterion(
|
return new NumberCriterion(
|
||||||
new MandatoryNumberCriterionOption(type, type)
|
new MandatoryNumberCriterionOption(type, type)
|
||||||
@@ -107,6 +108,8 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
return new TagsCriterion(ChildTagsCriterionOption);
|
return new TagsCriterion(ChildTagsCriterionOption);
|
||||||
case "performers":
|
case "performers":
|
||||||
return new PerformersCriterion();
|
return new PerformersCriterion();
|
||||||
|
case "performer_favorite":
|
||||||
|
return new PerformerFavoriteCriterion();
|
||||||
case "studios":
|
case "studios":
|
||||||
return new StudiosCriterion();
|
return new StudiosCriterion();
|
||||||
case "parent_studios":
|
case "parent_studios":
|
||||||
@@ -132,6 +135,8 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
);
|
);
|
||||||
case "phash":
|
case "phash":
|
||||||
return new StringCriterion(PhashCriterionOption);
|
return new StringCriterion(PhashCriterionOption);
|
||||||
|
case "duplicated":
|
||||||
|
return new DuplicatedCriterion();
|
||||||
case "ethnicity":
|
case "ethnicity":
|
||||||
case "country":
|
case "country":
|
||||||
case "hair_color":
|
case "hair_color":
|
||||||
|
|||||||
@@ -11,3 +11,15 @@ export class FavoriteCriterion extends BooleanCriterion {
|
|||||||
super(FavoriteCriterionOption);
|
super(FavoriteCriterionOption);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PerformerFavoriteCriterionOption = new BooleanCriterionOption(
|
||||||
|
"performer_favorite",
|
||||||
|
"performer_favorite",
|
||||||
|
"performer_favorite"
|
||||||
|
);
|
||||||
|
|
||||||
|
export class PerformerFavoriteCriterion extends BooleanCriterion {
|
||||||
|
constructor() {
|
||||||
|
super(PerformerFavoriteCriterionOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { CriterionModifier } from "src/core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import { CriterionOption, StringCriterion } from "./criterion";
|
import {
|
||||||
|
BooleanCriterionOption,
|
||||||
|
CriterionOption,
|
||||||
|
PhashDuplicateCriterion,
|
||||||
|
StringCriterion,
|
||||||
|
} from "./criterion";
|
||||||
|
|
||||||
export const PhashCriterionOption = new CriterionOption({
|
export const PhashCriterionOption = new CriterionOption({
|
||||||
messageID: "media_info.phash",
|
messageID: "media_info.phash",
|
||||||
@@ -19,3 +24,15 @@ export class PhashCriterion extends StringCriterion {
|
|||||||
super(PhashCriterionOption);
|
super(PhashCriterionOption);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DuplicatedCriterionOption = new BooleanCriterionOption(
|
||||||
|
"duplicated_phash",
|
||||||
|
"duplicated",
|
||||||
|
"duplicated"
|
||||||
|
);
|
||||||
|
|
||||||
|
export class DuplicatedCriterion extends PhashDuplicateCriterion {
|
||||||
|
constructor() {
|
||||||
|
super(DuplicatedCriterionOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { createStringCriterionOption } from "./criteria/criterion";
|
import {
|
||||||
|
createMandatoryNumberCriterionOption,
|
||||||
|
createStringCriterionOption,
|
||||||
|
} from "./criteria/criterion";
|
||||||
|
import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
|
||||||
import { GalleryIsMissingCriterionOption } from "./criteria/is-missing";
|
import { GalleryIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
import { OrganizedCriterionOption } from "./criteria/organized";
|
import { OrganizedCriterionOption } from "./criteria/organized";
|
||||||
import { PerformersCriterionOption } from "./criteria/performers";
|
import { PerformersCriterionOption } from "./criteria/performers";
|
||||||
@@ -47,6 +51,8 @@ const criterionOptions = [
|
|||||||
PerformerTagsCriterionOption,
|
PerformerTagsCriterionOption,
|
||||||
PerformersCriterionOption,
|
PerformersCriterionOption,
|
||||||
createStringCriterionOption("performer_count"),
|
createStringCriterionOption("performer_count"),
|
||||||
|
createMandatoryNumberCriterionOption("performer_age"),
|
||||||
|
PerformerFavoriteCriterionOption,
|
||||||
createStringCriterionOption("image_count"),
|
createStringCriterionOption("image_count"),
|
||||||
StudiosCriterionOption,
|
StudiosCriterionOption,
|
||||||
createStringCriterionOption("url"),
|
createStringCriterionOption("url"),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
createMandatoryStringCriterionOption,
|
createMandatoryStringCriterionOption,
|
||||||
createStringCriterionOption,
|
createStringCriterionOption,
|
||||||
} from "./criteria/criterion";
|
} from "./criteria/criterion";
|
||||||
|
import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
|
||||||
import { ImageIsMissingCriterionOption } from "./criteria/is-missing";
|
import { ImageIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
import { OrganizedCriterionOption } from "./criteria/organized";
|
import { OrganizedCriterionOption } from "./criteria/organized";
|
||||||
import { PerformersCriterionOption } from "./criteria/performers";
|
import { PerformersCriterionOption } from "./criteria/performers";
|
||||||
@@ -37,6 +38,8 @@ const criterionOptions = [
|
|||||||
PerformerTagsCriterionOption,
|
PerformerTagsCriterionOption,
|
||||||
PerformersCriterionOption,
|
PerformersCriterionOption,
|
||||||
createMandatoryNumberCriterionOption("performer_count"),
|
createMandatoryNumberCriterionOption("performer_count"),
|
||||||
|
createMandatoryNumberCriterionOption("performer_age"),
|
||||||
|
PerformerFavoriteCriterionOption,
|
||||||
StudiosCriterionOption,
|
StudiosCriterionOption,
|
||||||
];
|
];
|
||||||
export const ImageListFilterOptions = new ListFilterOptions(
|
export const ImageListFilterOptions = new ListFilterOptions(
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ import {
|
|||||||
} from "./criteria/tags";
|
} from "./criteria/tags";
|
||||||
import { ListFilterOptions, MediaSortByOptions } from "./filter-options";
|
import { ListFilterOptions, MediaSortByOptions } from "./filter-options";
|
||||||
import { DisplayMode } from "./types";
|
import { DisplayMode } from "./types";
|
||||||
import { PhashCriterionOption } from "./criteria/phash";
|
import {
|
||||||
|
DuplicatedCriterionOption,
|
||||||
|
PhashCriterionOption,
|
||||||
|
} from "./criteria/phash";
|
||||||
|
import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
|
||||||
|
|
||||||
const defaultSortBy = "date";
|
const defaultSortBy = "date";
|
||||||
const sortByOptions = [
|
const sortByOptions = [
|
||||||
@@ -32,6 +36,7 @@ const sortByOptions = [
|
|||||||
"movie_scene_number",
|
"movie_scene_number",
|
||||||
"interactive",
|
"interactive",
|
||||||
"interactive_speed",
|
"interactive_speed",
|
||||||
|
"perceptual_similarity",
|
||||||
...MediaSortByOptions,
|
...MediaSortByOptions,
|
||||||
].map(ListFilterOptions.createSortBy);
|
].map(ListFilterOptions.createSortBy);
|
||||||
|
|
||||||
@@ -53,6 +58,7 @@ const criterionOptions = [
|
|||||||
"checksum"
|
"checksum"
|
||||||
),
|
),
|
||||||
PhashCriterionOption,
|
PhashCriterionOption,
|
||||||
|
DuplicatedCriterionOption,
|
||||||
RatingCriterionOption,
|
RatingCriterionOption,
|
||||||
OrganizedCriterionOption,
|
OrganizedCriterionOption,
|
||||||
createMandatoryNumberCriterionOption("o_counter"),
|
createMandatoryNumberCriterionOption("o_counter"),
|
||||||
@@ -65,6 +71,8 @@ const criterionOptions = [
|
|||||||
PerformerTagsCriterionOption,
|
PerformerTagsCriterionOption,
|
||||||
PerformersCriterionOption,
|
PerformersCriterionOption,
|
||||||
createMandatoryNumberCriterionOption("performer_count"),
|
createMandatoryNumberCriterionOption("performer_count"),
|
||||||
|
createMandatoryNumberCriterionOption("performer_age"),
|
||||||
|
PerformerFavoriteCriterionOption,
|
||||||
StudiosCriterionOption,
|
StudiosCriterionOption,
|
||||||
MoviesCriterionOption,
|
MoviesCriterionOption,
|
||||||
createStringCriterionOption("url"),
|
createStringCriterionOption("url"),
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ export interface INumberValue {
|
|||||||
value2: number | undefined;
|
value2: number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPHashDuplicationValue {
|
||||||
|
duplicated: boolean;
|
||||||
|
distance?: number; // currently not implemented
|
||||||
|
}
|
||||||
|
|
||||||
export function criterionIsHierarchicalLabelValue(
|
export function criterionIsHierarchicalLabelValue(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
value: any
|
value: any
|
||||||
@@ -119,4 +124,7 @@ export type CriterionType =
|
|||||||
| "director"
|
| "director"
|
||||||
| "synopsis"
|
| "synopsis"
|
||||||
| "parent_tag_count"
|
| "parent_tag_count"
|
||||||
| "child_tag_count";
|
| "child_tag_count"
|
||||||
|
| "performer_favorite"
|
||||||
|
| "performer_age"
|
||||||
|
| "duplicated";
|
||||||
|
|||||||
Reference in New Issue
Block a user