mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
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:
parent
aeb68a5851
commit
14bde44597
14 changed files with 157 additions and 0 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
17
pkg/models/orientation.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
43
pkg/sqlite/criterion_handlers.go
Normal file
43
pkg/sqlite/criterion_handlers.go
Normal 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...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
32
ui/v2.5/src/models/list-filter/criteria/orientation.ts
Normal file
32
ui/v2.5/src/models/list-filter/criteria/orientation.ts
Normal 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"
|
||||||
|
);
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,7 @@ export type CriterionType =
|
||||||
| "details"
|
| "details"
|
||||||
| "title"
|
| "title"
|
||||||
| "oshash"
|
| "oshash"
|
||||||
|
| "orientation"
|
||||||
| "checksum"
|
| "checksum"
|
||||||
| "phash_distance"
|
| "phash_distance"
|
||||||
| "director"
|
| "director"
|
||||||
|
|
|
||||||
32
ui/v2.5/src/utils/orientation.ts
Normal file
32
ui/v2.5/src/utils/orientation.ts
Normal 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());
|
||||||
Loading…
Reference in a new issue