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:
kermieisinthehouse
2022-02-15 16:11:57 -08:00
committed by GitHub
parent 1d68492a5b
commit 4dd0bbc294
17 changed files with 250 additions and 59 deletions

View File

@@ -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
} }

View File

@@ -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)
}
} }
} }
} }

View File

@@ -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() {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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) { func getIntCriterionWhereClause(column string, input models.IntCriterionInput) (string, []interface{}) {
if modifier := criterionModifier.String(); criterionModifier.IsValid() { return getIntWhereClause(column, input.Modifier, input.Value, input.Value2)
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 getIntWhereClause(column string, modifier models.CriterionModifier, value int, upper *int) (string, []interface{}) {
binding, _ := getSimpleCriterionClause(input.Modifier, "?") if upper == nil {
var args []interface{} u := 0
upper = &u
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 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

View File

@@ -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))

View File

@@ -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",

View File

@@ -9,5 +9,6 @@
"ignore_organized": "Ignore organized scenes" "ignore_organized": "Ignore organized scenes"
} }
} }
} },
"performer_favorite": "Performer Favorited"
} }

View File

@@ -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",
};
}
}

View File

@@ -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":

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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"),

View File

@@ -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(

View File

@@ -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"),

View File

@@ -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";