From 14bde445975a9c489f3afee02ea1ea77cff44d29 Mon Sep 17 00:00:00 2001 From: keenbed <155155956+keenbed@users.noreply.github.com> Date: Tue, 16 Jan 2024 03:50:17 +0100 Subject: [PATCH] added support for image orientation filter (#4404) * added support for image orientation filter * Add orientation filtering to scenes --------- Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com> --- graphql/schema/types/filters.graphql | 17 ++++++++ pkg/models/filter.go | 4 ++ pkg/models/image.go | 2 + pkg/models/orientation.go | 17 ++++++++ pkg/models/scene.go | 2 + pkg/sqlite/criterion_handlers.go | 43 +++++++++++++++++++ pkg/sqlite/image.go | 1 + pkg/sqlite/scene.go | 1 + ui/v2.5/src/locales/en-GB.json | 1 + .../list-filter/criteria/orientation.ts | 32 ++++++++++++++ ui/v2.5/src/models/list-filter/images.ts | 2 + ui/v2.5/src/models/list-filter/scenes.ts | 2 + ui/v2.5/src/models/list-filter/types.ts | 1 + ui/v2.5/src/utils/orientation.ts | 32 ++++++++++++++ 14 files changed, 157 insertions(+) create mode 100644 pkg/models/orientation.go create mode 100644 pkg/sqlite/criterion_handlers.go create mode 100644 ui/v2.5/src/models/list-filter/criteria/orientation.ts create mode 100644 ui/v2.5/src/utils/orientation.ts diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index fa646d345..a52c4a6e6 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -61,6 +61,19 @@ input ResolutionCriterionInput { modifier: CriterionModifier! } +enum OrientationEnum { + "Landscape" + LANDSCAPE + "Portrait" + PORTRAIT + "Square" + SQUARE +} + +input OrientationCriterionInput { + value: [OrientationEnum!]! +} + input PHashDuplicationCriterionInput { duplicated: Boolean "Currently unimplemented" @@ -212,6 +225,8 @@ input SceneFilterType { duplicated: PHashDuplicationCriterionInput "Filter by resolution" resolution: ResolutionCriterionInput + "Filter by orientation" + orientation: OrientationCriterionInput "Filter by frame rate" framerate: IntCriterionInput "Filter by video codec" @@ -465,6 +480,8 @@ input ImageFilterType { o_counter: IntCriterionInput "Filter by resolution" resolution: ResolutionCriterionInput + "Filter by orientation" + orientation: OrientationCriterionInput "Filter to only include images missing this property" is_missing: String "Filter to only include images with this studio" diff --git a/pkg/models/filter.go b/pkg/models/filter.go index e9ddf7ab3..1513b0bbe 100644 --- a/pkg/models/filter.go +++ b/pkg/models/filter.go @@ -169,3 +169,7 @@ type PhashDistanceCriterionInput struct { Modifier CriterionModifier `json:"modifier"` Distance *int `json:"distance"` } + +type OrientationCriterionInput struct { + Value []OrientationEnum `json:"value"` +} diff --git a/pkg/models/image.go b/pkg/models/image.go index 8a8b5ba50..d4f85e4b6 100644 --- a/pkg/models/image.go +++ b/pkg/models/image.go @@ -29,6 +29,8 @@ type ImageFilterType struct { OCounter *IntCriterionInput `json:"o_counter"` // Filter by resolution Resolution *ResolutionCriterionInput `json:"resolution"` + // Filter by landscape/portrait + Orientation *OrientationCriterionInput `json:"orientation"` // Filter to only include images missing this property IsMissing *string `json:"is_missing"` // Filter to only include images with this studio diff --git a/pkg/models/orientation.go b/pkg/models/orientation.go new file mode 100644 index 000000000..eeb9febd3 --- /dev/null +++ b/pkg/models/orientation.go @@ -0,0 +1,17 @@ +package models + +type OrientationEnum string + +const ( + OrientationLandscape OrientationEnum = "LANDSCAPE" + OrientationPortrait OrientationEnum = "PORTRAIT" + OrientationSquare OrientationEnum = "SQUARE" +) + +func (e OrientationEnum) IsValid() bool { + switch e { + case OrientationLandscape, OrientationPortrait, OrientationSquare: + return true + } + return false +} diff --git a/pkg/models/scene.go b/pkg/models/scene.go index 748457a84..d0be3016b 100644 --- a/pkg/models/scene.go +++ b/pkg/models/scene.go @@ -39,6 +39,8 @@ type SceneFilterType struct { Duplicated *PHashDuplicationCriterionInput `json:"duplicated"` // Filter by resolution Resolution *ResolutionCriterionInput `json:"resolution"` + // Filter by orientation + Orientation *OrientationCriterionInput `json:"orientation"` // Filter by framerate Framerate *IntCriterionInput `json:"framerate"` // Filter by video codec diff --git a/pkg/sqlite/criterion_handlers.go b/pkg/sqlite/criterion_handlers.go new file mode 100644 index 000000000..5718947cb --- /dev/null +++ b/pkg/sqlite/criterion_handlers.go @@ -0,0 +1,43 @@ +package sqlite + +import ( + "context" + "fmt" + + "github.com/stashapp/stash/pkg/models" +) + +// shared criterion handlers go here + +func orientationCriterionHandler(orientation *models.OrientationCriterionInput, heightColumn string, widthColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc { + return func(ctx context.Context, f *filterBuilder) { + if orientation != nil { + if addJoinFn != nil { + addJoinFn(f) + } + + var clauses []sqlClause + + for _, v := range orientation.Value { + // width mod height + mod := "" + switch v { + case models.OrientationPortrait: + mod = "<" + case models.OrientationLandscape: + mod = ">" + case models.OrientationSquare: + mod = "=" + } + + if mod != "" { + clauses = append(clauses, makeClause(fmt.Sprintf("%s %s %s", widthColumn, mod, heightColumn))) + } + } + + if len(clauses) > 0 { + f.whereClauses = append(f.whereClauses, orClauses(clauses...)) + } + } + } +} diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index bfc9e6dc6..d32209225 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -709,6 +709,7 @@ func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageF query.handleCriterion(ctx, imageURLsCriterionHandler(imageFilter.URL)) query.handleCriterion(ctx, resolutionCriterionHandler(imageFilter.Resolution, "image_files.height", "image_files.width", qb.addImageFilesTable)) + query.handleCriterion(ctx, orientationCriterionHandler(imageFilter.Orientation, "image_files.height", "image_files.width", qb.addImageFilesTable)) query.handleCriterion(ctx, imageIsMissingCriterionHandler(qb, imageFilter.IsMissing)) query.handleCriterion(ctx, imageTagsCriterionHandler(qb, imageFilter.Tags)) diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index c32e76fdf..d003121bc 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -982,6 +982,7 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF query.handleCriterion(ctx, floatIntCriterionHandler(sceneFilter.Duration, "video_files.duration", qb.addVideoFilesTable)) query.handleCriterion(ctx, resolutionCriterionHandler(sceneFilter.Resolution, "video_files.height", "video_files.width", qb.addVideoFilesTable)) + query.handleCriterion(ctx, orientationCriterionHandler(sceneFilter.Orientation, "video_files.height", "video_files.width", qb.addVideoFilesTable)) query.handleCriterion(ctx, floatIntCriterionHandler(sceneFilter.Framerate, "ROUND(video_files.frame_rate)", qb.addVideoFilesTable)) query.handleCriterion(ctx, codecCriterionHandler(sceneFilter.VideoCodec, "video_files.video_codec", qb.addVideoFilesTable)) query.handleCriterion(ctx, codecCriterionHandler(sceneFilter.AudioCodec, "video_files.audio_codec", qb.addVideoFilesTable)) diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index bb2d5ca0a..fd73e5851 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -1076,6 +1076,7 @@ "none": "None", "o_counter": "O-Counter", "operations": "Operations", + "orientation": "Orientation", "organized": "Organised", "package_manager": { "add_source": "Add Source", diff --git a/ui/v2.5/src/models/list-filter/criteria/orientation.ts b/ui/v2.5/src/models/list-filter/criteria/orientation.ts new file mode 100644 index 000000000..011b9c694 --- /dev/null +++ b/ui/v2.5/src/models/list-filter/criteria/orientation.ts @@ -0,0 +1,32 @@ +import { orientationStrings, stringToOrientation } from "src/utils/orientation"; +import { CriterionType } from "../types"; +import { CriterionOption, MultiStringCriterion } from "./criterion"; +import { + OrientationCriterionInput, + OrientationEnum, +} from "src/core/generated-graphql"; + +export class OrientationCriterion extends MultiStringCriterion { + protected toCriterionInput(): OrientationCriterionInput { + return { + value: this.value + .map((v) => stringToOrientation(v)) + .filter((v) => v) as OrientationEnum[], + }; + } +} + +class BaseOrientationCriterionOption extends CriterionOption { + constructor(value: CriterionType) { + super({ + messageID: value, + type: value, + options: orientationStrings, + makeCriterion: () => new OrientationCriterion(this), + }); + } +} + +export const OrientationCriterionOption = new BaseOrientationCriterionOption( + "orientation" +); diff --git a/ui/v2.5/src/models/list-filter/images.ts b/ui/v2.5/src/models/list-filter/images.ts index d92f466d3..6f578a8a0 100644 --- a/ui/v2.5/src/models/list-filter/images.ts +++ b/ui/v2.5/src/models/list-filter/images.ts @@ -12,6 +12,7 @@ import { PathCriterionOption } from "./criteria/path"; import { PerformersCriterionOption } from "./criteria/performers"; import { RatingCriterionOption } from "./criteria/rating"; import { ResolutionCriterionOption } from "./criteria/resolution"; +import { OrientationCriterionOption } from "./criteria/orientation"; import { StudiosCriterionOption } from "./criteria/studios"; import { PerformerTagsCriterionOption, @@ -41,6 +42,7 @@ const criterionOptions = [ OrganizedCriterionOption, createMandatoryNumberCriterionOption("o_counter"), ResolutionCriterionOption, + OrientationCriterionOption, ImageIsMissingCriterionOption, TagsCriterionOption, RatingCriterionOption, diff --git a/ui/v2.5/src/models/list-filter/scenes.ts b/ui/v2.5/src/models/list-filter/scenes.ts index db2c28875..ca9dc3717 100644 --- a/ui/v2.5/src/models/list-filter/scenes.ts +++ b/ui/v2.5/src/models/list-filter/scenes.ts @@ -29,6 +29,7 @@ import { CaptionsCriterionOption } from "./criteria/captions"; import { StashIDCriterionOption } from "./criteria/stash-ids"; import { RatingCriterionOption } from "./criteria/rating"; import { PathCriterionOption } from "./criteria/path"; +import { OrientationCriterionOption } from "./criteria/orientation"; const defaultSortBy = "date"; const sortByOptions = [ @@ -72,6 +73,7 @@ const criterionOptions = [ RatingCriterionOption, createMandatoryNumberCriterionOption("o_counter"), ResolutionCriterionOption, + OrientationCriterionOption, createMandatoryNumberCriterionOption("framerate"), createStringCriterionOption("video_codec"), createStringCriterionOption("audio_codec"), diff --git a/ui/v2.5/src/models/list-filter/types.ts b/ui/v2.5/src/models/list-filter/types.ts index 55961ba1b..d0f803a73 100644 --- a/ui/v2.5/src/models/list-filter/types.ts +++ b/ui/v2.5/src/models/list-filter/types.ts @@ -173,6 +173,7 @@ export type CriterionType = | "details" | "title" | "oshash" + | "orientation" | "checksum" | "phash_distance" | "director" diff --git a/ui/v2.5/src/utils/orientation.ts b/ui/v2.5/src/utils/orientation.ts new file mode 100644 index 000000000..366b9c1a4 --- /dev/null +++ b/ui/v2.5/src/utils/orientation.ts @@ -0,0 +1,32 @@ +import { OrientationEnum } from "src/core/generated-graphql"; + +const stringOrientationMap = new Map([ + ["Landscape", OrientationEnum.Landscape], + ["Portrait", OrientationEnum.Portrait], + ["Square", OrientationEnum.Square], +]); + +export const stringToOrientation = ( + value?: string | null, + caseInsensitive?: boolean +) => { + if (!value) { + return undefined; + } + + const ret = stringOrientationMap.get(value); + if (ret || !caseInsensitive) { + return ret; + } + + const asUpper = value.toUpperCase(); + const foundEntry = Array.from(stringOrientationMap.entries()).find((e) => { + return e[0].toUpperCase() === asUpper; + }); + + if (foundEntry) { + return foundEntry[1]; + } +}; + +export const orientationStrings = Array.from(stringOrientationMap.keys());