diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index 6185c9895..874e16823 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -39,6 +39,14 @@ input PHashDuplicationCriterionInput { distance: Int } +input StashIDCriterionInput { + """If present, this value is treated as a predicate. + That is, it will filter based on stash_ids with the matching endpoint""" + endpoint: String + stash_id: String + modifier: CriterionModifier! +} + input PerformerFilterType { AND: PerformerFilterType OR: PerformerFilterType @@ -90,7 +98,9 @@ input PerformerFilterType { """Filter by gallery count""" gallery_count: IntCriterionInput """Filter by StashID""" - stash_id: StringCriterionInput + stash_id: StringCriterionInput @deprecated(reason: "Use stash_id_endpoint instead") + """Filter by StashID""" + stash_id_endpoint: StashIDCriterionInput """Filter by rating""" rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100") # rating expressed as 1-100 @@ -196,7 +206,9 @@ input SceneFilterType { """Filter by performer count""" performer_count: IntCriterionInput """Filter by StashID""" - stash_id: StringCriterionInput + stash_id: StringCriterionInput @deprecated(reason: "Use stash_id_endpoint instead") + """Filter by StashID""" + stash_id_endpoint: StashIDCriterionInput """Filter by url""" url: StringCriterionInput """Filter by interactive""" @@ -251,7 +263,9 @@ input StudioFilterType { """Filter to only include studios with this parent studio""" parents: MultiCriterionInput """Filter by StashID""" - stash_id: StringCriterionInput + stash_id: StringCriterionInput @deprecated(reason: "Use stash_id_endpoint instead") + """Filter by StashID""" + stash_id_endpoint: StashIDCriterionInput """Filter to only include studios missing this property""" is_missing: String """Filter by rating""" diff --git a/pkg/models/performer.go b/pkg/models/performer.go index b4379d41c..d5b6ea55c 100644 --- a/pkg/models/performer.go +++ b/pkg/models/performer.go @@ -111,6 +111,8 @@ type PerformerFilterType struct { GalleryCount *IntCriterionInput `json:"gallery_count"` // Filter by StashID StashID *StringCriterionInput `json:"stash_id"` + // Filter by StashID Endpoint + StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"` // Filter by rating expressed as 1-5 Rating *IntCriterionInput `json:"rating"` // Filter by rating expressed as 1-100 diff --git a/pkg/models/scene.go b/pkg/models/scene.go index 60220c6f0..3e6350658 100644 --- a/pkg/models/scene.go +++ b/pkg/models/scene.go @@ -69,6 +69,8 @@ type SceneFilterType struct { PerformerCount *IntCriterionInput `json:"performer_count"` // Filter by StashID StashID *StringCriterionInput `json:"stash_id"` + // Filter by StashID Endpoint + StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"` // Filter by url URL *StringCriterionInput `json:"url"` // Filter by interactive diff --git a/pkg/models/stash_ids.go b/pkg/models/stash_ids.go index 7b27a0637..9c9cfc56a 100644 --- a/pkg/models/stash_ids.go +++ b/pkg/models/stash_ids.go @@ -20,3 +20,11 @@ func (u *UpdateStashIDs) AddUnique(v StashID) { u.StashIDs = append(u.StashIDs, v) } + +type StashIDCriterionInput struct { + // If present, this value is treated as a predicate. + // That is, it will filter based on stash_ids with the matching endpoint + Endpoint *string `json:"endpoint"` + StashID *string `json:"stash_id"` + Modifier CriterionModifier `json:"modifier"` +} diff --git a/pkg/models/studio.go b/pkg/models/studio.go index 336898d29..26443edf7 100644 --- a/pkg/models/studio.go +++ b/pkg/models/studio.go @@ -12,6 +12,8 @@ type StudioFilterType struct { Parents *MultiCriterionInput `json:"parents"` // Filter by StashID StashID *StringCriterionInput `json:"stash_id"` + // Filter by StashID Endpoint + StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"` // Filter to only include studios missing this property IsMissing *string `json:"is_missing"` // Filter by rating expressed as 1-5 diff --git a/pkg/sqlite/filter.go b/pkg/sqlite/filter.go index c1eba93eb..d75012b4e 100644 --- a/pkg/sqlite/filter.go +++ b/pkg/sqlite/filter.go @@ -942,3 +942,39 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(criterion *mode } } } + +type stashIDCriterionHandler struct { + c *models.StashIDCriterionInput + stashIDRepository *stashIDRepository + stashIDTableAs string + parentIDCol string +} + +func (h *stashIDCriterionHandler) handle(ctx context.Context, f *filterBuilder) { + if h.c == nil { + return + } + + stashIDRepo := h.stashIDRepository + t := stashIDRepo.tableName + if h.stashIDTableAs != "" { + t = h.stashIDTableAs + } + + joinClause := fmt.Sprintf("%s.%s = %s", t, stashIDRepo.idColumn, h.parentIDCol) + if h.c.Endpoint != nil && *h.c.Endpoint != "" { + joinClause += fmt.Sprintf(" AND %s.endpoint = '%s'", t, *h.c.Endpoint) + } + + f.addLeftJoin(stashIDRepo.tableName, h.stashIDTableAs, joinClause) + + v := "" + if h.c.StashID != nil { + v = *h.c.StashID + } + + stringCriterionHandler(&models.StringCriterionInput{ + Value: v, + Modifier: h.c.Modifier, + }, t+".stash_id")(ctx, f) +} diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index fb938df4d..fae593ba6 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -533,6 +533,12 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform stringCriterionHandler(filter.StashID, "performer_stash_ids.stash_id")(ctx, f) } })) + query.handleCriterion(ctx, &stashIDCriterionHandler{ + c: filter.StashIDEndpoint, + stashIDRepository: qb.stashIDRepository(), + stashIDTableAs: "performer_stash_ids", + parentIDCol: "performers.id", + }) // TODO - need better handling of aliases query.handleCriterion(ctx, stringCriterionHandler(filter.Aliases, tableName+".aliases")) diff --git a/pkg/sqlite/performer_test.go b/pkg/sqlite/performer_test.go index 1a59a0575..934287524 100644 --- a/pkg/sqlite/performer_test.go +++ b/pkg/sqlite/performer_test.go @@ -585,6 +585,100 @@ func TestPerformerQueryIgnoreAutoTag(t *testing.T) { }) } +func TestPerformerQuery(t *testing.T) { + var ( + endpoint = performerStashID(performerIdxWithGallery).Endpoint + stashID = performerStashID(performerIdxWithGallery).StashID + ) + + tests := []struct { + name string + findFilter *models.FindFilterType + filter *models.PerformerFilterType + includeIdxs []int + excludeIdxs []int + wantErr bool + }{ + { + "stash id with endpoint", + nil, + &models.PerformerFilterType{ + StashIDEndpoint: &models.StashIDCriterionInput{ + Endpoint: &endpoint, + StashID: &stashID, + Modifier: models.CriterionModifierEquals, + }, + }, + []int{performerIdxWithGallery}, + nil, + false, + }, + { + "exclude stash id with endpoint", + nil, + &models.PerformerFilterType{ + StashIDEndpoint: &models.StashIDCriterionInput{ + Endpoint: &endpoint, + StashID: &stashID, + Modifier: models.CriterionModifierNotEquals, + }, + }, + nil, + []int{performerIdxWithGallery}, + false, + }, + { + "null stash id with endpoint", + nil, + &models.PerformerFilterType{ + StashIDEndpoint: &models.StashIDCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierIsNull, + }, + }, + nil, + []int{performerIdxWithGallery}, + false, + }, + { + "not null stash id with endpoint", + nil, + &models.PerformerFilterType{ + StashIDEndpoint: &models.StashIDCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierNotNull, + }, + }, + []int{performerIdxWithGallery}, + nil, + false, + }, + } + + for _, tt := range tests { + runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { + assert := assert.New(t) + + performers, _, err := db.Performer.Query(ctx, tt.filter, tt.findFilter) + if (err != nil) != tt.wantErr { + t.Errorf("PerformerStore.Query() error = %v, wantErr %v", err, tt.wantErr) + return + } + + ids := performersToIDs(performers) + include := indexesToIDs(performerIDs, tt.includeIdxs) + exclude := indexesToIDs(performerIDs, tt.excludeIdxs) + + for _, i := range include { + assert.Contains(ids, i) + } + for _, e := range exclude { + assert.NotContains(ids, e) + } + }) + } +} + func TestPerformerQueryForAutoTag(t *testing.T) { withTxn(func(ctx context.Context) error { tqb := db.Performer diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index 011216eeb..b26df92cb 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -864,6 +864,12 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF stringCriterionHandler(sceneFilter.StashID, "scene_stash_ids.stash_id")(ctx, f) } })) + query.handleCriterion(ctx, &stashIDCriterionHandler{ + c: sceneFilter.StashIDEndpoint, + stashIDRepository: qb.stashIDRepository(), + stashIDTableAs: "scene_stash_ids", + parentIDCol: "scenes.id", + }) query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Interactive, "video_files.interactive", qb.addVideoFilesTable)) query.handleCriterion(ctx, intCriterionHandler(sceneFilter.InteractiveSpeed, "video_files.interactive_speed", qb.addVideoFilesTable)) diff --git a/pkg/sqlite/scene_test.go b/pkg/sqlite/scene_test.go index fed6167e8..16442b7d3 100644 --- a/pkg/sqlite/scene_test.go +++ b/pkg/sqlite/scene_test.go @@ -2091,6 +2091,104 @@ func sceneQueryQ(ctx context.Context, t *testing.T, sqb models.SceneReader, q st assert.Len(t, scenes, totalScenes) } +func TestSceneQuery(t *testing.T) { + var ( + endpoint = sceneStashID(sceneIdxWithGallery).Endpoint + stashID = sceneStashID(sceneIdxWithGallery).StashID + ) + + tests := []struct { + name string + findFilter *models.FindFilterType + filter *models.SceneFilterType + includeIdxs []int + excludeIdxs []int + wantErr bool + }{ + { + "stash id with endpoint", + nil, + &models.SceneFilterType{ + StashIDEndpoint: &models.StashIDCriterionInput{ + Endpoint: &endpoint, + StashID: &stashID, + Modifier: models.CriterionModifierEquals, + }, + }, + []int{sceneIdxWithGallery}, + nil, + false, + }, + { + "exclude stash id with endpoint", + nil, + &models.SceneFilterType{ + StashIDEndpoint: &models.StashIDCriterionInput{ + Endpoint: &endpoint, + StashID: &stashID, + Modifier: models.CriterionModifierNotEquals, + }, + }, + nil, + []int{sceneIdxWithGallery}, + false, + }, + { + "null stash id with endpoint", + nil, + &models.SceneFilterType{ + StashIDEndpoint: &models.StashIDCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierIsNull, + }, + }, + nil, + []int{sceneIdxWithGallery}, + false, + }, + { + "not null stash id with endpoint", + nil, + &models.SceneFilterType{ + StashIDEndpoint: &models.StashIDCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierNotNull, + }, + }, + []int{sceneIdxWithGallery}, + nil, + false, + }, + } + + for _, tt := range tests { + runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { + assert := assert.New(t) + + results, err := db.Scene.Query(ctx, models.SceneQueryOptions{ + SceneFilter: tt.filter, + QueryOptions: models.QueryOptions{ + FindFilter: tt.findFilter, + }, + }) + if (err != nil) != tt.wantErr { + t.Errorf("PerformerStore.Query() error = %v, wantErr %v", err, tt.wantErr) + return + } + + include := indexesToIDs(performerIDs, tt.includeIdxs) + exclude := indexesToIDs(performerIDs, tt.excludeIdxs) + + for _, i := range include { + assert.Contains(results.IDs, i) + } + for _, e := range exclude { + assert.NotContains(results.IDs, e) + } + }) + } +} + func TestSceneQueryPath(t *testing.T) { const ( sceneIdx = 1 diff --git a/pkg/sqlite/studio.go b/pkg/sqlite/studio.go index ee1b1d731..8509b74d7 100644 --- a/pkg/sqlite/studio.go +++ b/pkg/sqlite/studio.go @@ -245,6 +245,12 @@ func (qb *studioQueryBuilder) makeFilter(ctx context.Context, studioFilter *mode stringCriterionHandler(studioFilter.StashID, "studio_stash_ids.stash_id")(ctx, f) } })) + query.handleCriterion(ctx, &stashIDCriterionHandler{ + c: studioFilter.StashIDEndpoint, + stashIDRepository: qb.stashIDRepository(), + stashIDTableAs: "studio_stash_ids", + parentIDCol: "studios.id", + }) query.handleCriterion(ctx, studioIsMissingCriterionHandler(qb, studioFilter.IsMissing)) query.handleCriterion(ctx, studioSceneCountCriterionHandler(qb, studioFilter.SceneCount)) diff --git a/ui/v2.5/src/components/List/AddFilterDialog.tsx b/ui/v2.5/src/components/List/AddFilterDialog.tsx index 032fa64fa..871214853 100644 --- a/ui/v2.5/src/components/List/AddFilterDialog.tsx +++ b/ui/v2.5/src/components/List/AddFilterDialog.tsx @@ -22,6 +22,7 @@ import { FormattedMessage, useIntl } from "react-intl"; import { criterionIsHierarchicalLabelValue, criterionIsNumberValue, + criterionIsStashIDValue, criterionIsDateValue, criterionIsTimestampValue, CriterionType, @@ -36,6 +37,8 @@ import { DateFilter } from "./Filters/DateFilter"; import { TimestampFilter } from "./Filters/TimestampFilter"; import { CountryCriterion } from "src/models/list-filter/criteria/country"; import { CountrySelect } from "../Shared"; +import { StashIDCriterion } from "src/models/list-filter/criteria/stash-ids"; +import { StashIDFilter } from "./Filters/StashIDFilter"; import { ConfigurationContext } from "src/hooks/Config"; import { RatingCriterion } from "../../models/list-filter/criteria/rating"; import { RatingFilter } from "./Filters/RatingFilter"; @@ -134,6 +137,16 @@ export const AddFilterDialog: React.FC = ({ } function renderSelect() { + // always show stashID filter + if (criterion instanceof StashIDCriterion) { + return ( + + ); + } + // Hide the value select if the modifier is "IsNull" or "NotNull" if ( criterion.modifier === CriterionModifier.IsNull || @@ -162,6 +175,7 @@ export const AddFilterDialog: React.FC = ({ options && !criterionIsHierarchicalLabelValue(criterion.value) && !criterionIsNumberValue(criterion.value) && + !criterionIsStashIDValue(criterion.value) && !criterionIsDateValue(criterion.value) && !criterionIsTimestampValue(criterion.value) && !Array.isArray(criterion.value) diff --git a/ui/v2.5/src/components/List/Filters/StashIDFilter.tsx b/ui/v2.5/src/components/List/Filters/StashIDFilter.tsx new file mode 100644 index 000000000..096f573b7 --- /dev/null +++ b/ui/v2.5/src/components/List/Filters/StashIDFilter.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { Form } from "react-bootstrap"; +import { useIntl } from "react-intl"; +import { IStashIDValue } from "../../../models/list-filter/types"; +import { Criterion } from "../../../models/list-filter/criteria/criterion"; +import { CriterionModifier } from "src/core/generated-graphql"; + +interface IStashIDFilterProps { + criterion: Criterion; + onValueChanged: (value: IStashIDValue) => void; +} + +export const StashIDFilter: React.FC = ({ + criterion, + onValueChanged, +}) => { + const intl = useIntl(); + + function onEndpointChanged(event: React.ChangeEvent) { + onValueChanged({ + endpoint: event.target.value, + stashID: criterion.value.stashID, + }); + } + + function onStashIDChanged(event: React.ChangeEvent) { + onValueChanged({ + stashID: event.target.value, + endpoint: criterion.value.endpoint, + }); + } + + return ( +
+ + + + {criterion.modifier !== CriterionModifier.IsNull && + criterion.modifier !== CriterionModifier.NotNull && ( + + + + )} +
+ ); +}; diff --git a/ui/v2.5/src/docs/en/Changelog/v0180.md b/ui/v2.5/src/docs/en/Changelog/v0180.md index 8a3c30974..9c3a3fea6 100644 --- a/ui/v2.5/src/docs/en/Changelog/v0180.md +++ b/ui/v2.5/src/docs/en/Changelog/v0180.md @@ -1,4 +1,5 @@ ### ✨ New Features +* Added support for filtering stash ids by endpoint. ([#3005](https://github.com/stashapp/stash/pull/3005)) * Added custom javascript option. ([#3132](https://github.com/stashapp/stash/pull/3132)) * Added ability to select rating system in the Interface settings, allowing 5 stars with full-, half- or quarter-stars, or numeric score out of 10 with one decimal point. ([#2830](https://github.com/stashapp/stash/pull/2830)) * Added filter criteria for Birthdate, Death Date, Date, Created At and Updated At fields. ([#2834](https://github.com/stashapp/stash/pull/2834)) diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index b3349c423..1e9a23f1a 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -1060,6 +1060,7 @@ "submit_update": "Already exists in {endpoint_name}" }, "statistics": "Statistics", + "stash_id_endpoint": "Stash ID Endpoint", "stats": { "image_size": "Images size", "scenes_duration": "Scenes duration", 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 9ecdb2915..a11027851 100644 --- a/ui/v2.5/src/models/list-filter/criteria/criterion.ts +++ b/ui/v2.5/src/models/list-filter/criteria/criterion.ts @@ -19,6 +19,7 @@ import { ILabeledValue, INumberValue, IOptionType, + IStashIDValue, IDateValue, ITimestampValue, } from "../types"; @@ -29,6 +30,7 @@ export type CriterionValue = | ILabeledId[] | IHierarchicalLabelValue | INumberValue + | IStashIDValue | IDateValue | ITimestampValue; diff --git a/ui/v2.5/src/models/list-filter/criteria/factory.ts b/ui/v2.5/src/models/list-filter/criteria/factory.ts index 124f426f6..53f50eec4 100644 --- a/ui/v2.5/src/models/list-filter/criteria/factory.ts +++ b/ui/v2.5/src/models/list-filter/criteria/factory.ts @@ -50,6 +50,7 @@ import { DuplicatedCriterion, PhashCriterionOption } from "./phash"; import { CaptionCriterion } from "./captions"; import { RatingCriterion } from "./rating"; import { CountryCriterion } from "./country"; +import { StashIDCriterion } from "./stash-ids"; import * as GQL from "src/core/generated-graphql"; import { IUIConfig } from "src/core/config"; import { defaultRatingSystemOptions } from "src/utils/rating"; @@ -169,6 +170,10 @@ export function makeCriteria( return new NumberCriterion( new NumberCriterionOption("height", "height_cm", type) ); + // stash_id is deprecated + case "stash_id": + case "stash_id_endpoint": + return new StashIDCriterion(); case "ethnicity": case "hair_color": case "eye_color": @@ -179,7 +184,6 @@ export function makeCriteria( case "piercings": case "aliases": case "url": - case "stash_id": case "details": case "title": case "director": diff --git a/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts b/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts new file mode 100644 index 000000000..6467c50ea --- /dev/null +++ b/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts @@ -0,0 +1,106 @@ +/* eslint @typescript-eslint/no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ +import { IntlShape } from "react-intl"; +import { + CriterionModifier, + StashIdCriterionInput, +} from "src/core/generated-graphql"; +import { IStashIDValue } from "../types"; +import { Criterion, CriterionOption } from "./criterion"; + +export const StashIDCriterionOption = new CriterionOption({ + messageID: "stash_id", + type: "stash_id_endpoint", + parameterName: "stash_id_endpoint", + modifierOptions: [ + CriterionModifier.Equals, + CriterionModifier.NotEquals, + CriterionModifier.IsNull, + CriterionModifier.NotNull, + ], +}); + +export class StashIDCriterion extends Criterion { + constructor() { + super(StashIDCriterionOption, { + endpoint: "", + stashID: "", + }); + } + + public get value(): IStashIDValue { + return this._value; + } + + public set value(newValue: string | IStashIDValue) { + // backwards compatibility - if this.value is a string, use that as stash_id + if (typeof newValue !== "object") { + this._value = { + endpoint: "", + stashID: newValue, + }; + } else { + this._value = newValue; + } + } + + protected toCriterionInput(): StashIdCriterionInput { + return { + endpoint: this.value.endpoint, + stash_id: this.value.stashID, + modifier: this.modifier, + }; + } + + public getLabel(intl: IntlShape): string { + const modifierString = Criterion.getModifierLabel(intl, this.modifier); + let valueString = ""; + + if ( + this.modifier !== CriterionModifier.IsNull && + this.modifier !== CriterionModifier.NotNull + ) { + valueString = this.getLabelValue(intl); + } else if (this.value.endpoint) { + valueString = "(" + this.value.endpoint + ")"; + } + + return intl.formatMessage( + { id: "criterion_modifier.format_string" }, + { + criterion: intl.formatMessage({ id: this.criterionOption.messageID }), + modifierString, + valueString, + } + ); + } + + public getLabelValue(_intl: IntlShape) { + let ret = this.value.stashID; + if (this.value.endpoint) { + ret += " (" + this.value.endpoint + ")"; + } + + return ret; + } + + public toJSON() { + let encodedCriterion; + if ( + (this.modifier === CriterionModifier.IsNull || + this.modifier === CriterionModifier.NotNull) && + !this.value.endpoint + ) { + encodedCriterion = { + type: this.criterionOption.type, + modifier: this.modifier, + }; + } else { + encodedCriterion = { + type: this.criterionOption.type, + value: this.value, + modifier: this.modifier, + }; + } + return JSON.stringify(encodedCriterion); + } +} diff --git a/ui/v2.5/src/models/list-filter/performers.ts b/ui/v2.5/src/models/list-filter/performers.ts index 046d25cfe..30d1d7316 100644 --- a/ui/v2.5/src/models/list-filter/performers.ts +++ b/ui/v2.5/src/models/list-filter/performers.ts @@ -11,6 +11,7 @@ import { import { FavoriteCriterionOption } from "./criteria/favorite"; import { GenderCriterionOption } from "./criteria/gender"; import { PerformerIsMissingCriterionOption } from "./criteria/is-missing"; +import { StashIDCriterionOption } from "./criteria/stash-ids"; import { StudiosCriterionOption } from "./criteria/studios"; import { TagsCriterionOption } from "./criteria/tags"; import { ListFilterOptions } from "./filter-options"; @@ -67,7 +68,6 @@ const stringCriteria: CriterionType[] = [ "tattoos", "piercings", "aliases", - "stash_id", ]; const criterionOptions = [ @@ -76,6 +76,7 @@ const criterionOptions = [ PerformerIsMissingCriterionOption, TagsCriterionOption, StudiosCriterionOption, + StashIDCriterionOption, createStringCriterionOption("url"), new NullNumberCriterionOption("rating", "rating100"), createMandatoryNumberCriterionOption("tag_count"), diff --git a/ui/v2.5/src/models/list-filter/scenes.ts b/ui/v2.5/src/models/list-filter/scenes.ts index c31daaf1f..5fd3142bf 100644 --- a/ui/v2.5/src/models/list-filter/scenes.ts +++ b/ui/v2.5/src/models/list-filter/scenes.ts @@ -26,6 +26,7 @@ import { } from "./criteria/phash"; import { PerformerFavoriteCriterionOption } from "./criteria/favorite"; import { CaptionsCriterionOption } from "./criteria/captions"; +import { StashIDCriterionOption } from "./criteria/stash-ids"; const defaultSortBy = "date"; const sortByOptions = [ @@ -82,7 +83,7 @@ const criterionOptions = [ StudiosCriterionOption, MoviesCriterionOption, createStringCriterionOption("url"), - createStringCriterionOption("stash_id"), + StashIDCriterionOption, InteractiveCriterionOption, CaptionsCriterionOption, createMandatoryNumberCriterionOption("interactive_speed"), diff --git a/ui/v2.5/src/models/list-filter/studios.ts b/ui/v2.5/src/models/list-filter/studios.ts index bf84ec192..89b26af7c 100644 --- a/ui/v2.5/src/models/list-filter/studios.ts +++ b/ui/v2.5/src/models/list-filter/studios.ts @@ -7,6 +7,7 @@ import { createMandatoryTimestampCriterionOption, } from "./criteria/criterion"; import { StudioIsMissingCriterionOption } from "./criteria/is-missing"; +import { StashIDCriterionOption } from "./criteria/stash-ids"; import { ParentStudiosCriterionOption } from "./criteria/studios"; import { ListFilterOptions } from "./filter-options"; import { DisplayMode } from "./types"; @@ -41,7 +42,7 @@ const criterionOptions = [ createMandatoryNumberCriterionOption("image_count"), createMandatoryNumberCriterionOption("gallery_count"), createStringCriterionOption("url"), - createStringCriterionOption("stash_id"), + StashIDCriterionOption, createStringCriterionOption("aliases"), createMandatoryTimestampCriterionOption("created_at"), createMandatoryTimestampCriterionOption("updated_at"), diff --git a/ui/v2.5/src/models/list-filter/types.ts b/ui/v2.5/src/models/list-filter/types.ts index debe78b77..21b81ef23 100644 --- a/ui/v2.5/src/models/list-filter/types.ts +++ b/ui/v2.5/src/models/list-filter/types.ts @@ -33,6 +33,11 @@ export interface IPHashDuplicationValue { distance?: number; // currently not implemented } +export interface IStashIDValue { + endpoint: string; + stashID: string; +} + export interface IDateValue { value: string; value2: string | undefined; @@ -57,6 +62,13 @@ export function criterionIsNumberValue( return typeof value === "object" && "value" in value && "value2" in value; } +export function criterionIsStashIDValue( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any +): value is IStashIDValue { + return typeof value === "object" && "endpoint" in value && "stashID" in value; +} + export function criterionIsDateValue( // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any @@ -151,6 +163,7 @@ export type CriterionType = | "duplicated" | "ignore_auto_tag" | "file_count" + | "stash_id_endpoint" | "date" | "created_at" | "updated_at"