diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index bb312e31d..4cf25d840 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -84,13 +84,23 @@ input PHashDuplicationCriterionInput { input StashIDCriterionInput { """ If present, this value is treated as a predicate. - That is, it will filter based on stash_ids with the matching endpoint + That is, it will filter based on stash_id with the matching endpoint """ endpoint: String stash_id: String modifier: CriterionModifier! } +input StashIDsCriterionInput { + """ + 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_ids: [String] + modifier: CriterionModifier! +} + input CustomFieldCriterionInput { field: String! value: [Any!] @@ -156,6 +166,9 @@ input PerformerFilterType { o_counter: IntCriterionInput "Filter by StashID" stash_id_endpoint: StashIDCriterionInput + @deprecated(reason: "use stash_ids_endpoint instead") + "Filter by StashIDs" + stash_ids_endpoint: StashIDsCriterionInput # rating expressed as 1-100 rating100: IntCriterionInput "Filter by url" @@ -292,6 +305,9 @@ input SceneFilterType { performer_count: IntCriterionInput "Filter by StashID" stash_id_endpoint: StashIDCriterionInput + @deprecated(reason: "use stash_ids_endpoint instead") + "Filter by StashIDs" + stash_ids_endpoint: StashIDsCriterionInput "Filter by url" url: StringCriterionInput "Filter by interactive" @@ -432,6 +448,9 @@ input StudioFilterType { parents: MultiCriterionInput "Filter by StashID" stash_id_endpoint: StashIDCriterionInput + @deprecated(reason: "use stash_ids_endpoint instead") + "Filter by StashIDs" + stash_ids_endpoint: StashIDsCriterionInput "Filter to only include studios with these tags" tags: HierarchicalMultiCriterionInput "Filter to only include studios missing this property" @@ -608,6 +627,10 @@ input TagFilterType { "Filter by StashID" stash_id_endpoint: StashIDCriterionInput + @deprecated(reason: "use stash_ids_endpoint instead") + + "Filter by StashID" + stash_ids_endpoint: StashIDsCriterionInput "Filter by related scenes that meet this criteria" scenes_filter: SceneFilterType diff --git a/pkg/models/performer.go b/pkg/models/performer.go index 239d8347f..63a08b30c 100644 --- a/pkg/models/performer.go +++ b/pkg/models/performer.go @@ -166,6 +166,8 @@ type PerformerFilterType struct { StashID *StringCriterionInput `json:"stash_id"` // Filter by StashID Endpoint StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"` + // Filter by StashIDs Endpoint + StashIDsEndpoint *StashIDsCriterionInput `json:"stash_ids_endpoint"` // Filter by rating expressed as 1-100 Rating100 *IntCriterionInput `json:"rating100"` // Filter by url diff --git a/pkg/models/scene.go b/pkg/models/scene.go index f0a863bf7..1c34967c6 100644 --- a/pkg/models/scene.go +++ b/pkg/models/scene.go @@ -79,6 +79,8 @@ type SceneFilterType struct { StashID *StringCriterionInput `json:"stash_id"` // Filter by StashID Endpoint StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"` + // Filter by StashIDs Endpoint + StashIDsEndpoint *StashIDsCriterionInput `json:"stash_ids_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 d761e959f..d73bfd880 100644 --- a/pkg/models/stash_ids.go +++ b/pkg/models/stash_ids.go @@ -129,8 +129,16 @@ func (u *UpdateStashIDs) Set(v StashID) { 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 + // That is, it will filter based on stash_id with the matching endpoint Endpoint *string `json:"endpoint"` StashID *string `json:"stash_id"` Modifier CriterionModifier `json:"modifier"` } + +type StashIDsCriterionInput 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"` + StashIDs []*string `json:"stash_ids"` + Modifier CriterionModifier `json:"modifier"` +} diff --git a/pkg/models/studio.go b/pkg/models/studio.go index 171168129..fd306b16c 100644 --- a/pkg/models/studio.go +++ b/pkg/models/studio.go @@ -10,6 +10,8 @@ type StudioFilterType struct { StashID *StringCriterionInput `json:"stash_id"` // Filter by StashID Endpoint StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"` + // Filter by StashIDs Endpoint + StashIDsEndpoint *StashIDsCriterionInput `json:"stash_ids_endpoint"` // Filter to only include studios missing this property IsMissing *string `json:"is_missing"` // Filter by rating expressed as 1-100 diff --git a/pkg/models/tag.go b/pkg/models/tag.go index 29b7e9be3..69d4f9e3c 100644 --- a/pkg/models/tag.go +++ b/pkg/models/tag.go @@ -42,6 +42,8 @@ type TagFilterType struct { IgnoreAutoTag *bool `json:"ignore_auto_tag"` // Filter by StashID Endpoint StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"` + // Filter by StashIDs Endpoint + StashIDsEndpoint *StashIDsCriterionInput `json:"stash_ids_endpoint"` // Filter by related scenes that meet this criteria ScenesFilter *SceneFilterType `json:"scenes_filter"` // Filter by related images that meet this criteria diff --git a/pkg/sqlite/criterion_handlers.go b/pkg/sqlite/criterion_handlers.go index fe6d1fcb5..c848f1a8b 100644 --- a/pkg/sqlite/criterion_handlers.go +++ b/pkg/sqlite/criterion_handlers.go @@ -1012,6 +1012,41 @@ func (h *stashIDCriterionHandler) handle(ctx context.Context, f *filterBuilder) return } + var stashIDs []*string + if h.c.StashID != nil { + stashIDs = []*string{h.c.StashID} + } else { + stashIDs = nil + } + + convertedInput := &models.StashIDsCriterionInput{ + Endpoint: h.c.Endpoint, + StashIDs: stashIDs, + Modifier: h.c.Modifier, + } + + convertedHandler := stashIDsCriterionHandler{ + c: convertedInput, + stashIDRepository: h.stashIDRepository, + stashIDTableAs: h.stashIDTableAs, + parentIDCol: h.parentIDCol, + } + + convertedHandler.handle(ctx, f) +} + +type stashIDsCriterionHandler struct { + c *models.StashIDsCriterionInput + stashIDRepository *stashIDRepository + stashIDTableAs string + parentIDCol string +} + +func (h *stashIDsCriterionHandler) handle(ctx context.Context, f *filterBuilder) { + if h.c == nil { + return + } + stashIDRepo := h.stashIDRepository t := stashIDRepo.tableName if h.stashIDTableAs != "" { @@ -1025,15 +1060,33 @@ func (h *stashIDCriterionHandler) handle(ctx context.Context, f *filterBuilder) f.addLeftJoin(stashIDRepo.tableName, h.stashIDTableAs, joinClause) - v := "" - if h.c.StashID != nil { - v = *h.c.StashID - } + if len(h.c.StashIDs) == 0 { + stringCriterionHandler(&models.StringCriterionInput{ + Value: "", + Modifier: h.c.Modifier, + }, t+".stash_id")(ctx, f) + } else { + b := f + for _, n := range h.c.StashIDs { + query := &filterBuilder{} + v := "" + if n != nil { + v = *n + } - stringCriterionHandler(&models.StringCriterionInput{ - Value: v, - Modifier: h.c.Modifier, - }, t+".stash_id")(ctx, f) + stringCriterionHandler(&models.StringCriterionInput{ + Value: v, + Modifier: h.c.Modifier, + }, t+".stash_id")(ctx, query) + + if h.c.Modifier == models.CriterionModifierNotEquals { + b.and(query) + } else { + b.or(query) + } + b = query + } + } } type relatedFilterHandler struct { diff --git a/pkg/sqlite/performer_filter.go b/pkg/sqlite/performer_filter.go index 11d3138bc..401664e33 100644 --- a/pkg/sqlite/performer_filter.go +++ b/pkg/sqlite/performer_filter.go @@ -148,6 +148,12 @@ func (qb *performerFilterHandler) criterionHandler() criterionHandler { stashIDTableAs: "performer_stash_ids", parentIDCol: "performers.id", }, + &stashIDsCriterionHandler{ + c: filter.StashIDsEndpoint, + stashIDRepository: &performerRepository.stashIDs, + stashIDTableAs: "performer_stash_ids", + parentIDCol: "performers.id", + }, qb.aliasCriterionHandler(filter.Aliases), diff --git a/pkg/sqlite/performer_test.go b/pkg/sqlite/performer_test.go index a88166657..8d53ca0db 100644 --- a/pkg/sqlite/performer_test.go +++ b/pkg/sqlite/performer_test.go @@ -1069,6 +1069,8 @@ func TestPerformerQuery(t *testing.T) { var ( endpoint = performerStashID(performerIdxWithGallery).Endpoint stashID = performerStashID(performerIdxWithGallery).StashID + stashID2 = performerStashID(performerIdx1WithGallery).StashID + stashIDs = []*string{&stashID, &stashID2} ) tests := []struct { @@ -1133,6 +1135,60 @@ func TestPerformerQuery(t *testing.T) { nil, false, }, + { + "stash ids with endpoint", + nil, + &models.PerformerFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + StashIDs: stashIDs, + Modifier: models.CriterionModifierEquals, + }, + }, + []int{performerIdxWithGallery, performerIdx1WithGallery}, + nil, + false, + }, + { + "exclude stash ids with endpoint", + nil, + &models.PerformerFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + StashIDs: stashIDs, + Modifier: models.CriterionModifierNotEquals, + }, + }, + nil, + []int{performerIdxWithGallery, performerIdx1WithGallery}, + false, + }, + { + "null stash ids with endpoint", + nil, + &models.PerformerFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierIsNull, + }, + }, + nil, + []int{performerIdxWithGallery, performerIdx1WithGallery}, + false, + }, + { + "not null stash ids with endpoint", + nil, + &models.PerformerFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierNotNull, + }, + }, + []int{performerIdxWithGallery, performerIdx1WithGallery}, + nil, + false, + }, { "circumcised (cut)", nil, diff --git a/pkg/sqlite/scene_filter.go b/pkg/sqlite/scene_filter.go index fad300248..72c75eca5 100644 --- a/pkg/sqlite/scene_filter.go +++ b/pkg/sqlite/scene_filter.go @@ -114,13 +114,18 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler { stringCriterionHandler(sceneFilter.StashID, "scene_stash_ids.stash_id")(ctx, f) } }), - &stashIDCriterionHandler{ c: sceneFilter.StashIDEndpoint, stashIDRepository: &sceneRepository.stashIDs, stashIDTableAs: "scene_stash_ids", parentIDCol: "scenes.id", }, + &stashIDsCriterionHandler{ + c: sceneFilter.StashIDsEndpoint, + stashIDRepository: &sceneRepository.stashIDs, + stashIDTableAs: "scene_stash_ids", + parentIDCol: "scenes.id", + }, boolCriterionHandler(sceneFilter.Interactive, "video_files.interactive", qb.addVideoFilesTable), intCriterionHandler(sceneFilter.InteractiveSpeed, "video_files.interactive_speed", qb.addVideoFilesTable), diff --git a/pkg/sqlite/scene_test.go b/pkg/sqlite/scene_test.go index 1efc4d705..df6676a0f 100644 --- a/pkg/sqlite/scene_test.go +++ b/pkg/sqlite/scene_test.go @@ -2098,6 +2098,8 @@ func TestSceneQuery(t *testing.T) { var ( endpoint = sceneStashID(sceneIdxWithGallery).Endpoint stashID = sceneStashID(sceneIdxWithGallery).StashID + stashID2 = sceneStashID(sceneIdxWithPerformer).StashID + stashIDs = []*string{&stashID, &stashID2} depth = -1 ) @@ -2203,6 +2205,60 @@ func TestSceneQuery(t *testing.T) { nil, false, }, + { + "stash ids with endpoint", + nil, + &models.SceneFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + StashIDs: stashIDs, + Modifier: models.CriterionModifierEquals, + }, + }, + []int{sceneIdxWithGallery, sceneIdxWithPerformer}, + nil, + false, + }, + { + "exclude stash ids with endpoint", + nil, + &models.SceneFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + StashIDs: stashIDs, + Modifier: models.CriterionModifierNotEquals, + }, + }, + nil, + []int{sceneIdxWithGallery, sceneIdxWithPerformer}, + false, + }, + { + "null stash ids with endpoint", + nil, + &models.SceneFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierIsNull, + }, + }, + nil, + []int{sceneIdxWithGallery, sceneIdxWithPerformer}, + false, + }, + { + "not null stash ids with endpoint", + nil, + &models.SceneFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierNotNull, + }, + }, + []int{sceneIdxWithGallery, sceneIdxWithPerformer}, + nil, + false, + }, { "with studio id 0 including child studios", nil, diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index 63c66fd06..7e6f821d1 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -1079,7 +1079,7 @@ func getObjectDate(index int) *models.Date { func sceneStashID(i int) models.StashID { return models.StashID{ StashID: getSceneStringValue(i, "stashid"), - Endpoint: getSceneStringValue(i, "endpoint"), + Endpoint: getSceneStringValue(0, "endpoint"), UpdatedAt: epochTime, } } @@ -1547,7 +1547,7 @@ func getIgnoreAutoTag(index int) bool { func performerStashID(i int) models.StashID { return models.StashID{ StashID: getPerformerStringValue(i, "stashid"), - Endpoint: getPerformerStringValue(i, "endpoint"), + Endpoint: getPerformerStringValue(0, "endpoint"), } } @@ -1700,7 +1700,7 @@ func getTagChildCount(id int) int { func tagStashID(i int) models.StashID { return models.StashID{ StashID: getTagStringValue(i, "stashid"), - Endpoint: getTagStringValue(i, "endpoint"), + Endpoint: getTagStringValue(0, "endpoint"), } } diff --git a/pkg/sqlite/studio_filter.go b/pkg/sqlite/studio_filter.go index 6ff7fcced..83a917701 100644 --- a/pkg/sqlite/studio_filter.go +++ b/pkg/sqlite/studio_filter.go @@ -72,6 +72,12 @@ func (qb *studioFilterHandler) criterionHandler() criterionHandler { stashIDTableAs: "studio_stash_ids", parentIDCol: "studios.id", }, + &stashIDsCriterionHandler{ + c: studioFilter.StashIDsEndpoint, + stashIDRepository: &studioRepository.stashIDs, + stashIDTableAs: "studio_stash_ids", + parentIDCol: "studios.id", + }, qb.isMissingCriterionHandler(studioFilter.IsMissing), qb.tagCountCriterionHandler(studioFilter.TagCount), diff --git a/pkg/sqlite/tag_filter.go b/pkg/sqlite/tag_filter.go index 27ccf3c09..344b7de91 100644 --- a/pkg/sqlite/tag_filter.go +++ b/pkg/sqlite/tag_filter.go @@ -91,6 +91,12 @@ func (qb *tagFilterHandler) criterionHandler() criterionHandler { stashIDTableAs: "tag_stash_ids", parentIDCol: "tags.id", }, + &stashIDsCriterionHandler{ + c: tagFilter.StashIDsEndpoint, + stashIDRepository: &tagRepository.stashIDs, + stashIDTableAs: "tag_stash_ids", + parentIDCol: "tags.id", + }, ×tampCriterionHandler{tagFilter.CreatedAt, "tags.created_at", nil}, ×tampCriterionHandler{tagFilter.UpdatedAt, "tags.updated_at", nil}, diff --git a/pkg/sqlite/tag_test.go b/pkg/sqlite/tag_test.go index 18fe486bc..f1bac19b2 100644 --- a/pkg/sqlite/tag_test.go +++ b/pkg/sqlite/tag_test.go @@ -356,6 +356,8 @@ func TestTagQuery(t *testing.T) { var ( endpoint = tagStashID(tagIdxWithPerformer).Endpoint stashID = tagStashID(tagIdxWithPerformer).StashID + stashID2 = tagStashID(tagIdx1WithPerformer).StashID + stashIDs = []*string{&stashID, &stashID2} ) tests := []struct { @@ -420,6 +422,60 @@ func TestTagQuery(t *testing.T) { nil, false, }, + { + "stash ids with endpoint", + nil, + &models.TagFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + StashIDs: stashIDs, + Modifier: models.CriterionModifierEquals, + }, + }, + []int{tagIdxWithPerformer, tagIdx1WithPerformer}, + nil, + false, + }, + { + "exclude stash ids with endpoint", + nil, + &models.TagFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + StashIDs: stashIDs, + Modifier: models.CriterionModifierNotEquals, + }, + }, + nil, + []int{tagIdxWithPerformer, tagIdx1WithPerformer}, + false, + }, + { + "null stash ids with endpoint", + nil, + &models.TagFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierIsNull, + }, + }, + nil, + []int{tagIdxWithPerformer, tagIdx1WithPerformer}, + false, + }, + { + "not null stash ids with endpoint", + nil, + &models.TagFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierNotNull, + }, + }, + []int{tagIdxWithPerformer, tagIdx1WithPerformer}, + nil, + false, + }, } for _, tt := range tests {