Add tag stash ids filter criterion (#6403)

* Add stash id filter to tag filter
* Add tag stash id criterion in UI
This commit is contained in:
WithoutPants
2025-12-12 08:54:57 +11:00
committed by GitHub
parent 25fdf676d2
commit 67b1dd8dd0
6 changed files with 131 additions and 0 deletions

View File

@@ -606,6 +606,9 @@ input TagFilterType {
"Filter by autotag ignore value"
ignore_auto_tag: Boolean
"Filter by StashID"
stash_id_endpoint: StashIDCriterionInput
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related images that meet this criteria"

View File

@@ -40,6 +40,8 @@ type TagFilterType struct {
ChildCount *IntCriterionInput `json:"child_count"`
// Filter by autotag ignore value
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
// Filter by StashID Endpoint
StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"`
// Filter by related scenes that meet this criteria
ScenesFilter *SceneFilterType `json:"scenes_filter"`
// Filter by related images that meet this criteria

View File

@@ -1688,6 +1688,13 @@ func getTagChildCount(id int) int {
return 0
}
func tagStashID(i int) models.StashID {
return models.StashID{
StashID: getTagStringValue(i, "stashid"),
Endpoint: getTagStringValue(i, "endpoint"),
}
}
// createTags creates n tags with plain Name and o tags with camel cased NaMe included
func createTags(ctx context.Context, tqb models.TagReaderWriter, n int, o int) error {
const namePlain = "Name"
@@ -1709,6 +1716,12 @@ func createTags(ctx context.Context, tqb models.TagReaderWriter, n int, o int) e
IgnoreAutoTag: getIgnoreAutoTag(i),
}
if (index+1)%5 != 0 {
tag.StashIDs = models.NewRelatedStashIDs([]models.StashID{
tagStashID(i),
})
}
err := tqb.Create(ctx, &tag)
if err != nil {

View File

@@ -84,6 +84,14 @@ func (qb *tagFilterHandler) criterionHandler() criterionHandler {
tagHierarchyHandler.ChildrenCriterionHandler(tagFilter.Children),
tagHierarchyHandler.ParentCountCriterionHandler(tagFilter.ParentCount),
tagHierarchyHandler.ChildCountCriterionHandler(tagFilter.ChildCount),
&stashIDCriterionHandler{
c: tagFilter.StashIDEndpoint,
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

@@ -343,6 +343,109 @@ func queryTags(ctx context.Context, t *testing.T, qb models.TagReader, tagFilter
return tags
}
func tagsToIDs(i []*models.Tag) []int {
ret := make([]int, len(i))
for i, v := range i {
ret[i] = v.ID
}
return ret
}
func TestTagQuery(t *testing.T) {
var (
endpoint = tagStashID(tagIdxWithPerformer).Endpoint
stashID = tagStashID(tagIdxWithPerformer).StashID
)
tests := []struct {
name string
findFilter *models.FindFilterType
filter *models.TagFilterType
includeIdxs []int
excludeIdxs []int
wantErr bool
}{
{
"stash id with endpoint",
nil,
&models.TagFilterType{
StashIDEndpoint: &models.StashIDCriterionInput{
Endpoint: &endpoint,
StashID: &stashID,
Modifier: models.CriterionModifierEquals,
},
},
[]int{tagIdxWithPerformer},
nil,
false,
},
{
"exclude stash id with endpoint",
nil,
&models.TagFilterType{
StashIDEndpoint: &models.StashIDCriterionInput{
Endpoint: &endpoint,
StashID: &stashID,
Modifier: models.CriterionModifierNotEquals,
},
},
nil,
[]int{tagIdxWithPerformer},
false,
},
{
"null stash id with endpoint",
nil,
&models.TagFilterType{
StashIDEndpoint: &models.StashIDCriterionInput{
Endpoint: &endpoint,
Modifier: models.CriterionModifierIsNull,
},
},
nil,
[]int{tagIdxWithPerformer},
false,
},
{
"not null stash id with endpoint",
nil,
&models.TagFilterType{
StashIDEndpoint: &models.StashIDCriterionInput{
Endpoint: &endpoint,
Modifier: models.CriterionModifierNotNull,
},
},
[]int{tagIdxWithPerformer},
nil,
false,
},
}
for _, tt := range tests {
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
assert := assert.New(t)
tags, _, err := db.Tag.Query(ctx, tt.filter, tt.findFilter)
if (err != nil) != tt.wantErr {
t.Errorf("PerformerStore.Query() error = %v, wantErr %v", err, tt.wantErr)
return
}
ids := tagsToIDs(tags)
include := indexesToIDs(tagIDs, tt.includeIdxs)
exclude := indexesToIDs(tagIDs, tt.excludeIdxs)
for _, i := range include {
assert.Contains(ids, i)
}
for _, e := range exclude {
assert.NotContains(ids, e)
}
})
}
}
func TestTagQueryIsMissingImage(t *testing.T) {
withTxn(func(ctx context.Context) error {
qb := db.Tag

View File

@@ -14,6 +14,7 @@ import {
ParentTagsCriterionOption,
} from "./criteria/tags";
import { FavoriteTagCriterionOption } from "./criteria/favorite";
import { StashIDCriterionOption } from "./criteria/stash-ids";
const defaultSortBy = "name";
const sortByOptions = ["name", "random", "scenes_duration"]
@@ -58,6 +59,7 @@ const criterionOptions = [
createStringCriterionOption("aliases"),
createStringCriterionOption("description"),
createBooleanCriterionOption("ignore_auto_tag"),
StashIDCriterionOption,
createMandatoryNumberCriterionOption("scene_count"),
createMandatoryNumberCriterionOption("image_count"),
createMandatoryNumberCriterionOption("gallery_count"),