Implement stash_ids_endpoint for the SceneFilterType (#6401)

* Implement stash_ids_endpoint for the SceneFilterType
* Reduce code duplication by calling the stashIDsCriterionHandler from the stashIDCriterionHandler
* Mark stash_id_endpoint in SceneFilterType, StudioFilterType, and PerformerFilterType as deprecated
This commit is contained in:
sashapp 2026-01-14 03:53:40 +00:00 committed by GitHub
parent 2a5b59a96a
commit ed3a239366
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 297 additions and 14 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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),

View file

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

View file

@ -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),

View file

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

View file

@ -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"),
}
}

View file

@ -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),

View file

@ -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",
},
&timestampCriterionHandler{tagFilter.CreatedAt, "tags.created_at", nil},
&timestampCriterionHandler{tagFilter.UpdatedAt, "tags.updated_at", nil},

View file

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