diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index 5e21a6acf..bf3517fae 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -33,6 +33,12 @@ input ResolutionCriterionInput { modifier: CriterionModifier! } +input PHashDuplicationCriterionInput { + duplicated: Boolean + """Currently unimplemented""" + distance: Int +} + input PerformerFilterType { AND: PerformerFilterType OR: PerformerFilterType @@ -130,6 +136,8 @@ input SceneFilterType { organized: Boolean """Filter by o-counter""" o_counter: IntCriterionInput + """Filter Scenes that have an exact phash match available""" + duplicated: PHashDuplicationCriterionInput """Filter by resolution""" resolution: ResolutionCriterionInput """Filter by duration (in seconds)""" @@ -148,6 +156,10 @@ input SceneFilterType { tag_count: IntCriterionInput """Filter to only include scenes with performers with these tags""" 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""" performers: MultiCriterionInput """Filter by performer count""" @@ -243,6 +255,10 @@ input GalleryFilterType { performers: MultiCriterionInput """Filter by performer count""" 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""" image_count: IntCriterionInput """Filter by url""" @@ -324,6 +340,8 @@ input ImageFilterType { performers: MultiCriterionInput """Filter by performer count""" performer_count: IntCriterionInput + """Filter images that have performers that have been favorited""" + performer_favorite: Boolean """Filter to only include images with these galleries""" galleries: MultiCriterionInput } diff --git a/pkg/sqlite/filter.go b/pkg/sqlite/filter.go index 8935140c2..9d3676542 100644 --- a/pkg/sqlite/filter.go +++ b/pkg/sqlite/filter.go @@ -391,13 +391,7 @@ func stringCriterionHandler(c *models.StringCriterionInput, column string) crite case models.CriterionModifierNotNull: f.addWhere("(" + column + " IS NOT NULL AND TRIM(" + column + ") != '')") default: - clause, count := getSimpleCriterionClause(modifier, "?") - - if count == 1 { - f.addWhere(column+" "+clause, c.Value) - } else { - f.addWhere(column + " " + clause) - } + panic("unsupported string filter modifier") } } } diff --git a/pkg/sqlite/gallery.go b/pkg/sqlite/gallery.go index f76f84d4c..7c8aca107 100644 --- a/pkg/sqlite/gallery.go +++ b/pkg/sqlite/gallery.go @@ -220,6 +220,8 @@ func (qb *galleryQueryBuilder) makeFilter(galleryFilter *models.GalleryFilterTyp query.handleCriterion(galleryPerformerTagsCriterionHandler(qb, galleryFilter.PerformerTags)) query.handleCriterion(galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution)) query.handleCriterion(galleryImageCountCriterionHandler(qb, galleryFilter.ImageCount)) + query.handleCriterion(galleryPerformerFavoriteCriterionHandler(galleryFilter.PerformerFavorite)) + query.handleCriterion(galleryPerformerAgeCriterionHandler(galleryFilter.PerformerAge)) 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 { return func(f *filterBuilder) { if resolution != nil && resolution.Value.IsValid() { diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index 480b9a6b9..0e84be497 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -248,6 +248,7 @@ func (qb *imageQueryBuilder) makeFilter(imageFilter *models.ImageFilterType) *fi query.handleCriterion(imagePerformerCountCriterionHandler(qb, imageFilter.PerformerCount)) query.handleCriterion(imageStudioCriterionHandler(qb, imageFilter.Studios)) query.handleCriterion(imagePerformerTagsCriterionHandler(qb, imageFilter.PerformerTags)) + query.handleCriterion(imagePerformerFavoriteCriterionHandler(imageFilter.PerformerFavorite)) return query } @@ -446,6 +447,26 @@ func imagePerformerCountCriterionHandler(qb *imageQueryBuilder, 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 { h := hierarchicalMultiCriterionHandlerBuilder{ tx: qb.tx, diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index 4981e43a5..2649a8322 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -392,6 +392,9 @@ func (qb *sceneQueryBuilder) makeFilter(sceneFilter *models.SceneFilterType) *fi query.handleCriterion(sceneStudioCriterionHandler(qb, sceneFilter.Studios)) query.handleCriterion(sceneMoviesCriterionHandler(qb, sceneFilter.Movies)) query.handleCriterion(scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags)) + query.handleCriterion(scenePerformerFavoriteCriterionHandler(sceneFilter.PerformerFavorite)) + query.handleCriterion(scenePerformerAgeCriterionHandler(sceneFilter.PerformerAge)) + query.handleCriterion(scenePhashDuplicatedCriterionHandler(sceneFilter.Duplicated)) 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 { return func(f *filterBuilder) { if durationFilter != nil { @@ -642,6 +660,43 @@ func scenePerformerCountCriterionHandler(qb *sceneQueryBuilder, 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 { h := hierarchicalMultiCriterionHandlerBuilder{ tx: qb.tx, diff --git a/pkg/sqlite/sql.go b/pkg/sqlite/sql.go index 5407492d1..dd7805676 100644 --- a/pkg/sqlite/sql.go +++ b/pkg/sqlite/sql.go @@ -9,7 +9,6 @@ import ( "strconv" "strings" - "github.com/stashapp/stash/pkg/logger" "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: colName := getColumn(tableName, "size") 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): // seed as a parameter from the UI // turn the provided seed into a float @@ -149,54 +152,39 @@ func getInBinding(length int) string { 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{}) { + return getIntWhereClause(column, input.Modifier, input.Value, input.Value2) } -func getIntCriterionWhereClause(column string, input models.IntCriterionInput) (string, []interface{}) { - binding, _ := getSimpleCriterionClause(input.Modifier, "?") - 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} +func getIntWhereClause(column string, modifier models.CriterionModifier, value int, upper *int) (string, []interface{}) { + if upper == nil { + u := 0 + upper = &u } - return column + " " + binding, args + 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 diff --git a/ui/v2.5/src/components/Changelog/versions/v0130.md b/ui/v2.5/src/components/Changelog/versions/v0130.md index 342bf0c5a..c5a4baeeb 100644 --- a/ui/v2.5/src/components/Changelog/versions/v0130.md +++ b/ui/v2.5/src/components/Changelog/versions/v0130.md @@ -1,4 +1,7 @@ ### ✨ 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)) * 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)) diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index a9b89287f..5cead8905 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -653,6 +653,7 @@ "search_accuracy_label": "Search Accuracy", "title": "Duplicate Scenes" }, + "duplicated_phash": "Duplicated (phash)", "duration": "Duration", "effect_filters": { "aspect": "Aspect", @@ -760,9 +761,12 @@ "parent_tags": "Parent Tags", "part_of": "Part of {parent}", "path": "Path", + "perceptual_similarity": "Perceptual Similarity (phash)", "performer": "Performer", "performerTags": "Performer Tags", "performer_count": "Performer Count", + "performer_favorite": "Performer Favourited", + "performer_age": "Performer Age", "performer_image": "Performer Image", "performers": "Performers", "piercings": "Piercings", diff --git a/ui/v2.5/src/locales/en-US.json b/ui/v2.5/src/locales/en-US.json index dc2af732f..fdf9c7b45 100644 --- a/ui/v2.5/src/locales/en-US.json +++ b/ui/v2.5/src/locales/en-US.json @@ -9,5 +9,6 @@ "ignore_organized": "Ignore organized scenes" } } - } + }, + "performer_favorite": "Performer Favorited" } diff --git a/ui/v2.5/src/models/list-filter/criteria/criterion.ts b/ui/v2.5/src/models/list-filter/criteria/criterion.ts index b40209795..e67ef95c0 100644 --- a/ui/v2.5/src/models/list-filter/criteria/criterion.ts +++ b/ui/v2.5/src/models/list-filter/criteria/criterion.ts @@ -6,6 +6,7 @@ import { HierarchicalMultiCriterionInput, IntCriterionInput, MultiCriterionInput, + PHashDuplicationCriterionInput, } from "src/core/generated-graphql"; import DurationUtils from "src/utils/duration"; import { @@ -521,3 +522,11 @@ export class DurationCriterion extends Criterion { : "?"; } } + +export class PhashDuplicateCriterion extends StringCriterion { + protected toCriterionInput(): PHashDuplicationCriterionInput { + return { + duplicated: this.value === "true", + }; + } +} diff --git a/ui/v2.5/src/models/list-filter/criteria/factory.ts b/ui/v2.5/src/models/list-filter/criteria/factory.ts index 82264e2ff..4c9cae2a3 100644 --- a/ui/v2.5/src/models/list-filter/criteria/factory.ts +++ b/ui/v2.5/src/models/list-filter/criteria/factory.ts @@ -10,7 +10,7 @@ import { ILabeledIdCriterion, } from "./criterion"; import { OrganizedCriterion } from "./organized"; -import { FavoriteCriterion } from "./favorite"; +import { FavoriteCriterion, PerformerFavoriteCriterion } from "./favorite"; import { HasMarkersCriterion } from "./has-markers"; import { PerformerIsMissingCriterionOption, @@ -40,7 +40,7 @@ import { GalleriesCriterion } from "./galleries"; import { CriterionType } from "../types"; import { InteractiveCriterion } from "./interactive"; import { RatingCriterionOption } from "./rating"; -import { PhashCriterionOption } from "./phash"; +import { DuplicatedCriterion, PhashCriterionOption } from "./phash"; export function makeCriteria(type: CriterionType = "none") { switch (type) { @@ -67,6 +67,7 @@ export function makeCriteria(type: CriterionType = "none") { case "image_count": case "gallery_count": case "performer_count": + case "performer_age": case "tag_count": return new NumberCriterion( new MandatoryNumberCriterionOption(type, type) @@ -107,6 +108,8 @@ export function makeCriteria(type: CriterionType = "none") { return new TagsCriterion(ChildTagsCriterionOption); case "performers": return new PerformersCriterion(); + case "performer_favorite": + return new PerformerFavoriteCriterion(); case "studios": return new StudiosCriterion(); case "parent_studios": @@ -132,6 +135,8 @@ export function makeCriteria(type: CriterionType = "none") { ); case "phash": return new StringCriterion(PhashCriterionOption); + case "duplicated": + return new DuplicatedCriterion(); case "ethnicity": case "country": case "hair_color": diff --git a/ui/v2.5/src/models/list-filter/criteria/favorite.ts b/ui/v2.5/src/models/list-filter/criteria/favorite.ts index 1d5f2c03a..362ebab93 100644 --- a/ui/v2.5/src/models/list-filter/criteria/favorite.ts +++ b/ui/v2.5/src/models/list-filter/criteria/favorite.ts @@ -11,3 +11,15 @@ export class FavoriteCriterion extends BooleanCriterion { super(FavoriteCriterionOption); } } + +export const PerformerFavoriteCriterionOption = new BooleanCriterionOption( + "performer_favorite", + "performer_favorite", + "performer_favorite" +); + +export class PerformerFavoriteCriterion extends BooleanCriterion { + constructor() { + super(PerformerFavoriteCriterionOption); + } +} diff --git a/ui/v2.5/src/models/list-filter/criteria/phash.ts b/ui/v2.5/src/models/list-filter/criteria/phash.ts index 25915e7e0..25bc8f6e7 100644 --- a/ui/v2.5/src/models/list-filter/criteria/phash.ts +++ b/ui/v2.5/src/models/list-filter/criteria/phash.ts @@ -1,5 +1,10 @@ import { CriterionModifier } from "src/core/generated-graphql"; -import { CriterionOption, StringCriterion } from "./criterion"; +import { + BooleanCriterionOption, + CriterionOption, + PhashDuplicateCriterion, + StringCriterion, +} from "./criterion"; export const PhashCriterionOption = new CriterionOption({ messageID: "media_info.phash", @@ -19,3 +24,15 @@ export class PhashCriterion extends StringCriterion { super(PhashCriterionOption); } } + +export const DuplicatedCriterionOption = new BooleanCriterionOption( + "duplicated_phash", + "duplicated", + "duplicated" +); + +export class DuplicatedCriterion extends PhashDuplicateCriterion { + constructor() { + super(DuplicatedCriterionOption); + } +} diff --git a/ui/v2.5/src/models/list-filter/galleries.ts b/ui/v2.5/src/models/list-filter/galleries.ts index 3845830e0..334c2685d 100644 --- a/ui/v2.5/src/models/list-filter/galleries.ts +++ b/ui/v2.5/src/models/list-filter/galleries.ts @@ -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 { OrganizedCriterionOption } from "./criteria/organized"; import { PerformersCriterionOption } from "./criteria/performers"; @@ -47,6 +51,8 @@ const criterionOptions = [ PerformerTagsCriterionOption, PerformersCriterionOption, createStringCriterionOption("performer_count"), + createMandatoryNumberCriterionOption("performer_age"), + PerformerFavoriteCriterionOption, createStringCriterionOption("image_count"), StudiosCriterionOption, createStringCriterionOption("url"), diff --git a/ui/v2.5/src/models/list-filter/images.ts b/ui/v2.5/src/models/list-filter/images.ts index 31589c6a3..0f675cd4c 100644 --- a/ui/v2.5/src/models/list-filter/images.ts +++ b/ui/v2.5/src/models/list-filter/images.ts @@ -3,6 +3,7 @@ import { createMandatoryStringCriterionOption, createStringCriterionOption, } from "./criteria/criterion"; +import { PerformerFavoriteCriterionOption } from "./criteria/favorite"; import { ImageIsMissingCriterionOption } from "./criteria/is-missing"; import { OrganizedCriterionOption } from "./criteria/organized"; import { PerformersCriterionOption } from "./criteria/performers"; @@ -37,6 +38,8 @@ const criterionOptions = [ PerformerTagsCriterionOption, PerformersCriterionOption, createMandatoryNumberCriterionOption("performer_count"), + createMandatoryNumberCriterionOption("performer_age"), + PerformerFavoriteCriterionOption, StudiosCriterionOption, ]; export const ImageListFilterOptions = new ListFilterOptions( diff --git a/ui/v2.5/src/models/list-filter/scenes.ts b/ui/v2.5/src/models/list-filter/scenes.ts index 88d76534b..71c91b966 100644 --- a/ui/v2.5/src/models/list-filter/scenes.ts +++ b/ui/v2.5/src/models/list-filter/scenes.ts @@ -18,7 +18,11 @@ import { } from "./criteria/tags"; import { ListFilterOptions, MediaSortByOptions } from "./filter-options"; import { DisplayMode } from "./types"; -import { PhashCriterionOption } from "./criteria/phash"; +import { + DuplicatedCriterionOption, + PhashCriterionOption, +} from "./criteria/phash"; +import { PerformerFavoriteCriterionOption } from "./criteria/favorite"; const defaultSortBy = "date"; const sortByOptions = [ @@ -32,6 +36,7 @@ const sortByOptions = [ "movie_scene_number", "interactive", "interactive_speed", + "perceptual_similarity", ...MediaSortByOptions, ].map(ListFilterOptions.createSortBy); @@ -53,6 +58,7 @@ const criterionOptions = [ "checksum" ), PhashCriterionOption, + DuplicatedCriterionOption, RatingCriterionOption, OrganizedCriterionOption, createMandatoryNumberCriterionOption("o_counter"), @@ -65,6 +71,8 @@ const criterionOptions = [ PerformerTagsCriterionOption, PerformersCriterionOption, createMandatoryNumberCriterionOption("performer_count"), + createMandatoryNumberCriterionOption("performer_age"), + PerformerFavoriteCriterionOption, StudiosCriterionOption, MoviesCriterionOption, createStringCriterionOption("url"), diff --git a/ui/v2.5/src/models/list-filter/types.ts b/ui/v2.5/src/models/list-filter/types.ts index 817c81237..50009a76e 100644 --- a/ui/v2.5/src/models/list-filter/types.ts +++ b/ui/v2.5/src/models/list-filter/types.ts @@ -28,6 +28,11 @@ export interface INumberValue { value2: number | undefined; } +export interface IPHashDuplicationValue { + duplicated: boolean; + distance?: number; // currently not implemented +} + export function criterionIsHierarchicalLabelValue( // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any @@ -119,4 +124,7 @@ export type CriterionType = | "director" | "synopsis" | "parent_tag_count" - | "child_tag_count"; + | "child_tag_count" + | "performer_favorite" + | "performer_age" + | "duplicated";