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>
This commit is contained in:
keenbed 2024-01-16 03:50:17 +01:00 committed by GitHub
parent aeb68a5851
commit 14bde44597
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 157 additions and 0 deletions

View file

@ -61,6 +61,19 @@ input ResolutionCriterionInput {
modifier: CriterionModifier! modifier: CriterionModifier!
} }
enum OrientationEnum {
"Landscape"
LANDSCAPE
"Portrait"
PORTRAIT
"Square"
SQUARE
}
input OrientationCriterionInput {
value: [OrientationEnum!]!
}
input PHashDuplicationCriterionInput { input PHashDuplicationCriterionInput {
duplicated: Boolean duplicated: Boolean
"Currently unimplemented" "Currently unimplemented"
@ -212,6 +225,8 @@ input SceneFilterType {
duplicated: PHashDuplicationCriterionInput duplicated: PHashDuplicationCriterionInput
"Filter by resolution" "Filter by resolution"
resolution: ResolutionCriterionInput resolution: ResolutionCriterionInput
"Filter by orientation"
orientation: OrientationCriterionInput
"Filter by frame rate" "Filter by frame rate"
framerate: IntCriterionInput framerate: IntCriterionInput
"Filter by video codec" "Filter by video codec"
@ -465,6 +480,8 @@ input ImageFilterType {
o_counter: IntCriterionInput o_counter: IntCriterionInput
"Filter by resolution" "Filter by resolution"
resolution: ResolutionCriterionInput resolution: ResolutionCriterionInput
"Filter by orientation"
orientation: OrientationCriterionInput
"Filter to only include images missing this property" "Filter to only include images missing this property"
is_missing: String is_missing: String
"Filter to only include images with this studio" "Filter to only include images with this studio"

View file

@ -169,3 +169,7 @@ type PhashDistanceCriterionInput struct {
Modifier CriterionModifier `json:"modifier"` Modifier CriterionModifier `json:"modifier"`
Distance *int `json:"distance"` Distance *int `json:"distance"`
} }
type OrientationCriterionInput struct {
Value []OrientationEnum `json:"value"`
}

View file

@ -29,6 +29,8 @@ type ImageFilterType struct {
OCounter *IntCriterionInput `json:"o_counter"` OCounter *IntCriterionInput `json:"o_counter"`
// Filter by resolution // Filter by resolution
Resolution *ResolutionCriterionInput `json:"resolution"` Resolution *ResolutionCriterionInput `json:"resolution"`
// Filter by landscape/portrait
Orientation *OrientationCriterionInput `json:"orientation"`
// Filter to only include images missing this property // Filter to only include images missing this property
IsMissing *string `json:"is_missing"` IsMissing *string `json:"is_missing"`
// Filter to only include images with this studio // Filter to only include images with this studio

17
pkg/models/orientation.go Normal file
View file

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

View file

@ -39,6 +39,8 @@ type SceneFilterType struct {
Duplicated *PHashDuplicationCriterionInput `json:"duplicated"` Duplicated *PHashDuplicationCriterionInput `json:"duplicated"`
// Filter by resolution // Filter by resolution
Resolution *ResolutionCriterionInput `json:"resolution"` Resolution *ResolutionCriterionInput `json:"resolution"`
// Filter by orientation
Orientation *OrientationCriterionInput `json:"orientation"`
// Filter by framerate // Filter by framerate
Framerate *IntCriterionInput `json:"framerate"` Framerate *IntCriterionInput `json:"framerate"`
// Filter by video codec // Filter by video codec

View file

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

View file

@ -709,6 +709,7 @@ func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageF
query.handleCriterion(ctx, imageURLsCriterionHandler(imageFilter.URL)) query.handleCriterion(ctx, imageURLsCriterionHandler(imageFilter.URL))
query.handleCriterion(ctx, resolutionCriterionHandler(imageFilter.Resolution, "image_files.height", "image_files.width", qb.addImageFilesTable)) 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, imageIsMissingCriterionHandler(qb, imageFilter.IsMissing))
query.handleCriterion(ctx, imageTagsCriterionHandler(qb, imageFilter.Tags)) query.handleCriterion(ctx, imageTagsCriterionHandler(qb, imageFilter.Tags))

View file

@ -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, 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, 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, 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.VideoCodec, "video_files.video_codec", qb.addVideoFilesTable))
query.handleCriterion(ctx, codecCriterionHandler(sceneFilter.AudioCodec, "video_files.audio_codec", qb.addVideoFilesTable)) query.handleCriterion(ctx, codecCriterionHandler(sceneFilter.AudioCodec, "video_files.audio_codec", qb.addVideoFilesTable))

View file

@ -1076,6 +1076,7 @@
"none": "None", "none": "None",
"o_counter": "O-Counter", "o_counter": "O-Counter",
"operations": "Operations", "operations": "Operations",
"orientation": "Orientation",
"organized": "Organised", "organized": "Organised",
"package_manager": { "package_manager": {
"add_source": "Add Source", "add_source": "Add Source",

View file

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

View file

@ -12,6 +12,7 @@ import { PathCriterionOption } from "./criteria/path";
import { PerformersCriterionOption } from "./criteria/performers"; import { PerformersCriterionOption } from "./criteria/performers";
import { RatingCriterionOption } from "./criteria/rating"; import { RatingCriterionOption } from "./criteria/rating";
import { ResolutionCriterionOption } from "./criteria/resolution"; import { ResolutionCriterionOption } from "./criteria/resolution";
import { OrientationCriterionOption } from "./criteria/orientation";
import { StudiosCriterionOption } from "./criteria/studios"; import { StudiosCriterionOption } from "./criteria/studios";
import { import {
PerformerTagsCriterionOption, PerformerTagsCriterionOption,
@ -41,6 +42,7 @@ const criterionOptions = [
OrganizedCriterionOption, OrganizedCriterionOption,
createMandatoryNumberCriterionOption("o_counter"), createMandatoryNumberCriterionOption("o_counter"),
ResolutionCriterionOption, ResolutionCriterionOption,
OrientationCriterionOption,
ImageIsMissingCriterionOption, ImageIsMissingCriterionOption,
TagsCriterionOption, TagsCriterionOption,
RatingCriterionOption, RatingCriterionOption,

View file

@ -29,6 +29,7 @@ import { CaptionsCriterionOption } from "./criteria/captions";
import { StashIDCriterionOption } from "./criteria/stash-ids"; import { StashIDCriterionOption } from "./criteria/stash-ids";
import { RatingCriterionOption } from "./criteria/rating"; import { RatingCriterionOption } from "./criteria/rating";
import { PathCriterionOption } from "./criteria/path"; import { PathCriterionOption } from "./criteria/path";
import { OrientationCriterionOption } from "./criteria/orientation";
const defaultSortBy = "date"; const defaultSortBy = "date";
const sortByOptions = [ const sortByOptions = [
@ -72,6 +73,7 @@ const criterionOptions = [
RatingCriterionOption, RatingCriterionOption,
createMandatoryNumberCriterionOption("o_counter"), createMandatoryNumberCriterionOption("o_counter"),
ResolutionCriterionOption, ResolutionCriterionOption,
OrientationCriterionOption,
createMandatoryNumberCriterionOption("framerate"), createMandatoryNumberCriterionOption("framerate"),
createStringCriterionOption("video_codec"), createStringCriterionOption("video_codec"),
createStringCriterionOption("audio_codec"), createStringCriterionOption("audio_codec"),

View file

@ -173,6 +173,7 @@ export type CriterionType =
| "details" | "details"
| "title" | "title"
| "oshash" | "oshash"
| "orientation"
| "checksum" | "checksum"
| "phash_distance" | "phash_distance"
| "director" | "director"

View file

@ -0,0 +1,32 @@
import { OrientationEnum } from "src/core/generated-graphql";
const stringOrientationMap = new Map<string, OrientationEnum>([
["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());