diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index cda7adb6d..2f77975b6 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -64,6 +64,8 @@ input SceneMarkerFilterType { } input SceneFilterType { + """Filter by path""" + path: StringCriterionInput """Filter by rating""" rating: IntCriterionInput """Filter by o-counter""" @@ -101,6 +103,8 @@ input StudioFilterType { } input GalleryFilterType { + """Filter by path""" + path: StringCriterionInput """Filter to only include galleries missing this property""" is_missing: String } diff --git a/pkg/models/querybuilder_gallery.go b/pkg/models/querybuilder_gallery.go index c5fdf532c..fdf9fd73c 100644 --- a/pkg/models/querybuilder_gallery.go +++ b/pkg/models/querybuilder_gallery.go @@ -154,6 +154,8 @@ func (qb *GalleryQueryBuilder) Query(galleryFilter *GalleryFilterType, findFilte query.addArg(thisArgs...) } + query.handleStringCriterionInput(galleryFilter.Path, "galleries.path") + if isMissingFilter := galleryFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" { switch *isMissingFilter { case "scene": diff --git a/pkg/models/querybuilder_gallery_test.go b/pkg/models/querybuilder_gallery_test.go index ad2234092..16b04d009 100644 --- a/pkg/models/querybuilder_gallery_test.go +++ b/pkg/models/querybuilder_gallery_test.go @@ -125,6 +125,34 @@ func galleryQueryQ(t *testing.T, qb models.GalleryQueryBuilder, q string, expect assert.Len(t, galleries, totalGalleries) } +func TestGalleryQueryPath(t *testing.T) { + const galleryIdx = 1 + galleryPath := getGalleryStringValue(galleryIdx, "Path") + + pathCriterion := models.StringCriterionInput{ + Value: galleryPath, + Modifier: models.CriterionModifierEquals, + } + + verifyGalleriesPath(t, pathCriterion) + + pathCriterion.Modifier = models.CriterionModifierNotEquals + verifyGalleriesPath(t, pathCriterion) +} + +func verifyGalleriesPath(t *testing.T, pathCriterion models.StringCriterionInput) { + sqb := models.NewGalleryQueryBuilder() + galleryFilter := models.GalleryFilterType{ + Path: &pathCriterion, + } + + galleries, _ := sqb.Query(&galleryFilter, nil) + + for _, gallery := range galleries { + verifyString(t, gallery.Path, pathCriterion) + } +} + func TestGalleryQueryIsMissingScene(t *testing.T) { qb := models.NewGalleryQueryBuilder() isMissing := "scene" diff --git a/pkg/models/querybuilder_performer.go b/pkg/models/querybuilder_performer.go index ef86580e1..321b38e11 100644 --- a/pkg/models/querybuilder_performer.go +++ b/pkg/models/querybuilder_performer.go @@ -206,18 +206,18 @@ func (qb *PerformerQueryBuilder) Query(performerFilter *PerformerFilterType, fin } } - handleStringCriterion(tableName+".ethnicity", performerFilter.Ethnicity, &query) - handleStringCriterion(tableName+".country", performerFilter.Country, &query) - handleStringCriterion(tableName+".eye_color", performerFilter.EyeColor, &query) - handleStringCriterion(tableName+".height", performerFilter.Height, &query) - handleStringCriterion(tableName+".measurements", performerFilter.Measurements, &query) - handleStringCriterion(tableName+".fake_tits", performerFilter.FakeTits, &query) - handleStringCriterion(tableName+".career_length", performerFilter.CareerLength, &query) - handleStringCriterion(tableName+".tattoos", performerFilter.Tattoos, &query) - handleStringCriterion(tableName+".piercings", performerFilter.Piercings, &query) + query.handleStringCriterionInput(performerFilter.Ethnicity, tableName+".ethnicity") + query.handleStringCriterionInput(performerFilter.Country, tableName+".country") + query.handleStringCriterionInput(performerFilter.EyeColor, tableName+".eye_color") + query.handleStringCriterionInput(performerFilter.Height, tableName+".height") + query.handleStringCriterionInput(performerFilter.Measurements, tableName+".measurements") + query.handleStringCriterionInput(performerFilter.FakeTits, tableName+".fake_tits") + query.handleStringCriterionInput(performerFilter.CareerLength, tableName+".career_length") + query.handleStringCriterionInput(performerFilter.Tattoos, tableName+".tattoos") + query.handleStringCriterionInput(performerFilter.Piercings, tableName+".piercings") // TODO - need better handling of aliases - handleStringCriterion(tableName+".aliases", performerFilter.Aliases, &query) + query.handleStringCriterionInput(performerFilter.Aliases, tableName+".aliases") query.sortAndPagination = qb.getPerformerSort(findFilter) + getPagination(findFilter) idsResult, countResult := query.executeFind() @@ -231,27 +231,6 @@ func (qb *PerformerQueryBuilder) Query(performerFilter *PerformerFilterType, fin return performers, countResult } -func handleStringCriterion(column string, value *StringCriterionInput, query *queryBuilder) { - if value != nil { - if modifier := value.Modifier.String(); value.Modifier.IsValid() { - switch modifier { - case "EQUALS": - clause, thisArgs := getSearchBinding([]string{column}, value.Value, false) - query.addWhere(clause) - query.addArg(thisArgs...) - case "NOT_EQUALS": - clause, thisArgs := getSearchBinding([]string{column}, value.Value, true) - query.addWhere(clause) - query.addArg(thisArgs...) - case "IS_NULL": - query.addWhere(column + " IS NULL") - case "NOT_NULL": - query.addWhere(column + " IS NOT NULL") - } - } - } -} - func getBirthYearFilterClause(criterionModifier CriterionModifier, value int) ([]string, []interface{}) { var clauses []string var args []interface{} diff --git a/pkg/models/querybuilder_scene.go b/pkg/models/querybuilder_scene.go index 4f18ab7a5..164d63e69 100644 --- a/pkg/models/querybuilder_scene.go +++ b/pkg/models/querybuilder_scene.go @@ -312,21 +312,9 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin query.addArg(thisArgs...) } - if rating := sceneFilter.Rating; rating != nil { - clause, count := getIntCriterionWhereClause("scenes.rating", *sceneFilter.Rating) - query.addWhere(clause) - if count == 1 { - query.addArg(sceneFilter.Rating.Value) - } - } - - if oCounter := sceneFilter.OCounter; oCounter != nil { - clause, count := getIntCriterionWhereClause("scenes.o_counter", *sceneFilter.OCounter) - query.addWhere(clause) - if count == 1 { - query.addArg(sceneFilter.OCounter.Value) - } - } + query.handleStringCriterionInput(sceneFilter.Path, "scenes.path") + query.handleIntCriterionInput(sceneFilter.Rating, "scenes.rating") + query.handleIntCriterionInput(sceneFilter.OCounter, "scenes.o_counter") if durationFilter := sceneFilter.Duration; durationFilter != nil { clause, thisArgs := getDurationWhereClause(*durationFilter) diff --git a/pkg/models/querybuilder_scene_test.go b/pkg/models/querybuilder_scene_test.go index c86e7c3e6..06b300674 100644 --- a/pkg/models/querybuilder_scene_test.go +++ b/pkg/models/querybuilder_scene_test.go @@ -135,6 +135,62 @@ func sceneQueryQ(t *testing.T, sqb models.SceneQueryBuilder, q string, expectedS assert.Len(t, scenes, totalScenes) } +func TestSceneQueryPath(t *testing.T) { + const sceneIdx = 1 + scenePath := getSceneStringValue(sceneIdx, "Path") + + pathCriterion := models.StringCriterionInput{ + Value: scenePath, + Modifier: models.CriterionModifierEquals, + } + + verifyScenesPath(t, pathCriterion) + + pathCriterion.Modifier = models.CriterionModifierNotEquals + verifyScenesPath(t, pathCriterion) +} + +func verifyScenesPath(t *testing.T, pathCriterion models.StringCriterionInput) { + sqb := models.NewSceneQueryBuilder() + sceneFilter := models.SceneFilterType{ + Path: &pathCriterion, + } + + scenes, _ := sqb.Query(&sceneFilter, nil) + + for _, scene := range scenes { + verifyString(t, scene.Path, pathCriterion) + } +} + +func verifyNullString(t *testing.T, value sql.NullString, criterion models.StringCriterionInput) { + t.Helper() + assert := assert.New(t) + if criterion.Modifier == models.CriterionModifierIsNull { + assert.False(value.Valid, "expect is null values to be null") + } + if criterion.Modifier == models.CriterionModifierNotNull { + assert.True(value.Valid, "expect is null values to be null") + } + if criterion.Modifier == models.CriterionModifierEquals { + assert.Equal(criterion.Value, value.String) + } + if criterion.Modifier == models.CriterionModifierNotEquals { + assert.NotEqual(criterion.Value, value.String) + } +} + +func verifyString(t *testing.T, value string, criterion models.StringCriterionInput) { + t.Helper() + assert := assert.New(t) + if criterion.Modifier == models.CriterionModifierEquals { + assert.Equal(criterion.Value, value) + } + if criterion.Modifier == models.CriterionModifierNotEquals { + assert.NotEqual(criterion.Value, value) + } +} + func TestSceneQueryRating(t *testing.T) { const rating = 3 ratingCriterion := models.IntCriterionInput{ diff --git a/pkg/models/querybuilder_sql.go b/pkg/models/querybuilder_sql.go index 300884491..78e719292 100644 --- a/pkg/models/querybuilder_sql.go +++ b/pkg/models/querybuilder_sql.go @@ -48,6 +48,37 @@ func (qb *queryBuilder) addArg(args ...interface{}) { qb.args = append(qb.args, args...) } +func (qb *queryBuilder) handleIntCriterionInput(c *IntCriterionInput, column string) { + if c != nil { + clause, count := getIntCriterionWhereClause(column, *c) + qb.addWhere(clause) + if count == 1 { + qb.addArg(c.Value) + } + } +} + +func (qb *queryBuilder) handleStringCriterionInput(c *StringCriterionInput, column string) { + if c != nil { + if modifier := c.Modifier.String(); c.Modifier.IsValid() { + switch modifier { + case "EQUALS": + clause, thisArgs := getSearchBinding([]string{column}, c.Value, false) + qb.addWhere(clause) + qb.addArg(thisArgs...) + case "NOT_EQUALS": + clause, thisArgs := getSearchBinding([]string{column}, c.Value, true) + qb.addWhere(clause) + qb.addArg(thisArgs...) + case "IS_NULL": + qb.addWhere(column + " IS NULL") + case "NOT_NULL": + qb.addWhere(column + " IS NOT NULL") + } + } + } +} + var randomSortFloat = rand.Float64() func selectAll(tableName string) string { diff --git a/ui/v2.5/src/components/Changelog/versions/v040.md b/ui/v2.5/src/components/Changelog/versions/v040.md index 4573898bb..c778db807 100644 --- a/ui/v2.5/src/components/Changelog/versions/v040.md +++ b/ui/v2.5/src/components/Changelog/versions/v040.md @@ -3,6 +3,7 @@ * Add selective scene export. ### 🎨 Improvements +* Add path filter to scene and gallery query. * Add button to hide left panel on scene page. * Add link to parent studio in studio page. * Add missing scenes movie filter. 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 cfc6af532..803f046c0 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 { ILabeledId, ILabeledValue, IOptionType } from "../types"; export type CriterionType = | "none" + | "path" | "rating" | "o_counter" | "resolution" @@ -48,6 +49,8 @@ export abstract class Criterion { switch (type) { case "none": return "None"; + case "path": + return "Path"; case "rating": return "Rating"; case "o_counter": @@ -237,10 +240,10 @@ export class StringCriterion extends Criterion { public parameterName: string; public modifier = CriterionModifier.Equals; public modifierOptions = [ - Criterion.getModifierOption(CriterionModifier.Equals), - Criterion.getModifierOption(CriterionModifier.NotEquals), - Criterion.getModifierOption(CriterionModifier.IsNull), - Criterion.getModifierOption(CriterionModifier.NotNull), + StringCriterion.getModifierOption(CriterionModifier.Equals), + StringCriterion.getModifierOption(CriterionModifier.NotEquals), + StringCriterion.getModifierOption(CriterionModifier.IsNull), + StringCriterion.getModifierOption(CriterionModifier.NotNull), ]; public options: string[] | undefined; public value: string = ""; @@ -262,6 +265,43 @@ export class StringCriterion extends Criterion { this.parameterName = type; } } + + public static getModifierOption( + modifier: CriterionModifier = CriterionModifier.Equals + ): ILabeledValue { + switch (modifier) { + case CriterionModifier.Equals: + return { value: CriterionModifier.Equals, label: "Includes" }; + case CriterionModifier.NotEquals: + return { value: CriterionModifier.NotEquals, label: "Excludes" }; + default: + return super.getModifierOption(modifier); + } + } + + public getLabel(): string { + let modifierString: string; + switch (this.modifier) { + case CriterionModifier.Equals: + modifierString = "includes"; + break; + case CriterionModifier.NotEquals: + modifierString = "excludes"; + break; + default: + return this.getLabel(); + } + + const valueString = this.getLabelValue(); + return `${Criterion.getLabel(this.type)} ${modifierString} ${valueString}`; + } +} + +export class MandatoryStringCriterion extends StringCriterion { + public modifierOptions = [ + StringCriterion.getModifierOption(CriterionModifier.Equals), + StringCriterion.getModifierOption(CriterionModifier.NotEquals), + ]; } export class NumberCriterion extends Criterion { diff --git a/ui/v2.5/src/models/list-filter/criteria/utils.ts b/ui/v2.5/src/models/list-filter/criteria/utils.ts index 13ef6d774..2e886b33f 100644 --- a/ui/v2.5/src/models/list-filter/criteria/utils.ts +++ b/ui/v2.5/src/models/list-filter/criteria/utils.ts @@ -6,6 +6,7 @@ import { StringCriterion, NumberCriterion, DurationCriterion, + MandatoryStringCriterion, } from "./criterion"; import { FavoriteCriterion } from "./favorite"; import { HasMarkersCriterion } from "./has-markers"; @@ -30,6 +31,8 @@ export function makeCriteria(type: CriterionType = "none") { switch (type) { case "none": return new NoneCriterion(); + case "path": + return new MandatoryStringCriterion(type, type); case "rating": return new RatingCriterion(); case "o_counter": diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index 237c2802e..343c360d5 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -20,6 +20,7 @@ import { NumberCriterion, StringCriterion, DurationCriterion, + MandatoryStringCriterion, } from "./criteria/criterion"; import { FavoriteCriterion, @@ -124,6 +125,7 @@ export class ListFilterModel { ]; this.criterionOptions = [ new NoneCriterionOption(), + ListFilterModel.createCriterionOption("path"), new RatingCriterionOption(), ListFilterModel.createCriterionOption("o_counter"), new ResolutionCriterionOption(), @@ -199,6 +201,7 @@ export class ListFilterModel { this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List]; this.criterionOptions = [ new NoneCriterionOption(), + ListFilterModel.createCriterionOption("path"), new GalleryIsMissingCriterionOption(), ]; break; @@ -380,6 +383,14 @@ export class ListFilterModel { const result: SceneFilterType = {}; this.criteria.forEach((criterion) => { switch (criterion.type) { + case "path": { + const pathCrit = criterion as MandatoryStringCriterion; + result.path = { + value: pathCrit.value, + modifier: pathCrit.modifier, + }; + break; + } case "rating": { const ratingCrit = criterion as RatingCriterion; result.rating = { @@ -647,6 +658,14 @@ export class ListFilterModel { const result: GalleryFilterType = {}; this.criteria.forEach((criterion) => { switch (criterion.type) { + case "path": { + const pathCrit = criterion as MandatoryStringCriterion; + result.path = { + value: pathCrit.value, + modifier: pathCrit.modifier, + }; + break; + } case "galleryIsMissing": result.is_missing = (criterion as IsMissingCriterion).value; break;