mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Path filter for scenes and galleries (#834)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user