mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Support filtering by StashID endpoint (#3005)
* Add endpoint to stash_id filter in UI Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -39,6 +39,14 @@ input PHashDuplicationCriterionInput {
|
|||||||
distance: Int
|
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 {
|
input PerformerFilterType {
|
||||||
AND: PerformerFilterType
|
AND: PerformerFilterType
|
||||||
OR: PerformerFilterType
|
OR: PerformerFilterType
|
||||||
@@ -90,7 +98,9 @@ input PerformerFilterType {
|
|||||||
"""Filter by gallery count"""
|
"""Filter by gallery count"""
|
||||||
gallery_count: IntCriterionInput
|
gallery_count: IntCriterionInput
|
||||||
"""Filter by StashID"""
|
"""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"""
|
"""Filter by rating"""
|
||||||
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
||||||
# rating expressed as 1-100
|
# rating expressed as 1-100
|
||||||
@@ -196,7 +206,9 @@ input SceneFilterType {
|
|||||||
"""Filter by performer count"""
|
"""Filter by performer count"""
|
||||||
performer_count: IntCriterionInput
|
performer_count: IntCriterionInput
|
||||||
"""Filter by StashID"""
|
"""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"""
|
"""Filter by url"""
|
||||||
url: StringCriterionInput
|
url: StringCriterionInput
|
||||||
"""Filter by interactive"""
|
"""Filter by interactive"""
|
||||||
@@ -251,7 +263,9 @@ input StudioFilterType {
|
|||||||
"""Filter to only include studios with this parent studio"""
|
"""Filter to only include studios with this parent studio"""
|
||||||
parents: MultiCriterionInput
|
parents: MultiCriterionInput
|
||||||
"""Filter by StashID"""
|
"""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"""
|
"""Filter to only include studios missing this property"""
|
||||||
is_missing: String
|
is_missing: String
|
||||||
"""Filter by rating"""
|
"""Filter by rating"""
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ type PerformerFilterType struct {
|
|||||||
GalleryCount *IntCriterionInput `json:"gallery_count"`
|
GalleryCount *IntCriterionInput `json:"gallery_count"`
|
||||||
// Filter by StashID
|
// Filter by StashID
|
||||||
StashID *StringCriterionInput `json:"stash_id"`
|
StashID *StringCriterionInput `json:"stash_id"`
|
||||||
|
// Filter by StashID Endpoint
|
||||||
|
StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"`
|
||||||
// Filter by rating expressed as 1-5
|
// Filter by rating expressed as 1-5
|
||||||
Rating *IntCriterionInput `json:"rating"`
|
Rating *IntCriterionInput `json:"rating"`
|
||||||
// Filter by rating expressed as 1-100
|
// Filter by rating expressed as 1-100
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ type SceneFilterType struct {
|
|||||||
PerformerCount *IntCriterionInput `json:"performer_count"`
|
PerformerCount *IntCriterionInput `json:"performer_count"`
|
||||||
// Filter by StashID
|
// Filter by StashID
|
||||||
StashID *StringCriterionInput `json:"stash_id"`
|
StashID *StringCriterionInput `json:"stash_id"`
|
||||||
|
// Filter by StashID Endpoint
|
||||||
|
StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"`
|
||||||
// Filter by url
|
// Filter by url
|
||||||
URL *StringCriterionInput `json:"url"`
|
URL *StringCriterionInput `json:"url"`
|
||||||
// Filter by interactive
|
// Filter by interactive
|
||||||
|
|||||||
@@ -20,3 +20,11 @@ func (u *UpdateStashIDs) AddUnique(v StashID) {
|
|||||||
|
|
||||||
u.StashIDs = append(u.StashIDs, v)
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ type StudioFilterType struct {
|
|||||||
Parents *MultiCriterionInput `json:"parents"`
|
Parents *MultiCriterionInput `json:"parents"`
|
||||||
// Filter by StashID
|
// Filter by StashID
|
||||||
StashID *StringCriterionInput `json:"stash_id"`
|
StashID *StringCriterionInput `json:"stash_id"`
|
||||||
|
// Filter by StashID Endpoint
|
||||||
|
StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"`
|
||||||
// Filter to only include studios missing this property
|
// Filter to only include studios missing this property
|
||||||
IsMissing *string `json:"is_missing"`
|
IsMissing *string `json:"is_missing"`
|
||||||
// Filter by rating expressed as 1-5
|
// Filter by rating expressed as 1-5
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -533,6 +533,12 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform
|
|||||||
stringCriterionHandler(filter.StashID, "performer_stash_ids.stash_id")(ctx, f)
|
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
|
// TODO - need better handling of aliases
|
||||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Aliases, tableName+".aliases"))
|
query.handleCriterion(ctx, stringCriterionHandler(filter.Aliases, tableName+".aliases"))
|
||||||
|
|||||||
@@ -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) {
|
func TestPerformerQueryForAutoTag(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
withTxn(func(ctx context.Context) error {
|
||||||
tqb := db.Performer
|
tqb := db.Performer
|
||||||
|
|||||||
@@ -864,6 +864,12 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF
|
|||||||
stringCriterionHandler(sceneFilter.StashID, "scene_stash_ids.stash_id")(ctx, f)
|
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, boolCriterionHandler(sceneFilter.Interactive, "video_files.interactive", qb.addVideoFilesTable))
|
||||||
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.InteractiveSpeed, "video_files.interactive_speed", qb.addVideoFilesTable))
|
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.InteractiveSpeed, "video_files.interactive_speed", qb.addVideoFilesTable))
|
||||||
|
|||||||
@@ -2091,6 +2091,104 @@ func sceneQueryQ(ctx context.Context, t *testing.T, sqb models.SceneReader, q st
|
|||||||
assert.Len(t, scenes, totalScenes)
|
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) {
|
func TestSceneQueryPath(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
sceneIdx = 1
|
sceneIdx = 1
|
||||||
|
|||||||
@@ -245,6 +245,12 @@ func (qb *studioQueryBuilder) makeFilter(ctx context.Context, studioFilter *mode
|
|||||||
stringCriterionHandler(studioFilter.StashID, "studio_stash_ids.stash_id")(ctx, f)
|
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, studioIsMissingCriterionHandler(qb, studioFilter.IsMissing))
|
||||||
query.handleCriterion(ctx, studioSceneCountCriterionHandler(qb, studioFilter.SceneCount))
|
query.handleCriterion(ctx, studioSceneCountCriterionHandler(qb, studioFilter.SceneCount))
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||||||
import {
|
import {
|
||||||
criterionIsHierarchicalLabelValue,
|
criterionIsHierarchicalLabelValue,
|
||||||
criterionIsNumberValue,
|
criterionIsNumberValue,
|
||||||
|
criterionIsStashIDValue,
|
||||||
criterionIsDateValue,
|
criterionIsDateValue,
|
||||||
criterionIsTimestampValue,
|
criterionIsTimestampValue,
|
||||||
CriterionType,
|
CriterionType,
|
||||||
@@ -36,6 +37,8 @@ import { DateFilter } from "./Filters/DateFilter";
|
|||||||
import { TimestampFilter } from "./Filters/TimestampFilter";
|
import { TimestampFilter } from "./Filters/TimestampFilter";
|
||||||
import { CountryCriterion } from "src/models/list-filter/criteria/country";
|
import { CountryCriterion } from "src/models/list-filter/criteria/country";
|
||||||
import { CountrySelect } from "../Shared";
|
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 { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { RatingCriterion } from "../../models/list-filter/criteria/rating";
|
import { RatingCriterion } from "../../models/list-filter/criteria/rating";
|
||||||
import { RatingFilter } from "./Filters/RatingFilter";
|
import { RatingFilter } from "./Filters/RatingFilter";
|
||||||
@@ -134,6 +137,16 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSelect() {
|
function renderSelect() {
|
||||||
|
// always show stashID filter
|
||||||
|
if (criterion instanceof StashIDCriterion) {
|
||||||
|
return (
|
||||||
|
<StashIDFilter
|
||||||
|
criterion={criterion}
|
||||||
|
onValueChanged={onValueChanged}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Hide the value select if the modifier is "IsNull" or "NotNull"
|
// Hide the value select if the modifier is "IsNull" or "NotNull"
|
||||||
if (
|
if (
|
||||||
criterion.modifier === CriterionModifier.IsNull ||
|
criterion.modifier === CriterionModifier.IsNull ||
|
||||||
@@ -162,6 +175,7 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
|||||||
options &&
|
options &&
|
||||||
!criterionIsHierarchicalLabelValue(criterion.value) &&
|
!criterionIsHierarchicalLabelValue(criterion.value) &&
|
||||||
!criterionIsNumberValue(criterion.value) &&
|
!criterionIsNumberValue(criterion.value) &&
|
||||||
|
!criterionIsStashIDValue(criterion.value) &&
|
||||||
!criterionIsDateValue(criterion.value) &&
|
!criterionIsDateValue(criterion.value) &&
|
||||||
!criterionIsTimestampValue(criterion.value) &&
|
!criterionIsTimestampValue(criterion.value) &&
|
||||||
!Array.isArray(criterion.value)
|
!Array.isArray(criterion.value)
|
||||||
|
|||||||
56
ui/v2.5/src/components/List/Filters/StashIDFilter.tsx
Normal file
56
ui/v2.5/src/components/List/Filters/StashIDFilter.tsx
Normal file
@@ -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<IStashIDValue>;
|
||||||
|
onValueChanged: (value: IStashIDValue) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StashIDFilter: React.FC<IStashIDFilterProps> = ({
|
||||||
|
criterion,
|
||||||
|
onValueChanged,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
function onEndpointChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
onValueChanged({
|
||||||
|
endpoint: event.target.value,
|
||||||
|
stashID: criterion.value.stashID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStashIDChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
onValueChanged({
|
||||||
|
stashID: event.target.value,
|
||||||
|
endpoint: criterion.value.endpoint,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Control
|
||||||
|
className="btn-secondary"
|
||||||
|
onBlur={onEndpointChanged}
|
||||||
|
defaultValue={criterion.value ? criterion.value.endpoint : ""}
|
||||||
|
placeholder={intl.formatMessage({ id: "stash_id_endpoint" })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
{criterion.modifier !== CriterionModifier.IsNull &&
|
||||||
|
criterion.modifier !== CriterionModifier.NotNull && (
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Control
|
||||||
|
className="btn-secondary"
|
||||||
|
onBlur={onStashIDChanged}
|
||||||
|
defaultValue={criterion.value ? criterion.value.stashID : ""}
|
||||||
|
placeholder={intl.formatMessage({ id: "stash_id" })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
### ✨ New Features
|
### ✨ 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 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 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))
|
* Added filter criteria for Birthdate, Death Date, Date, Created At and Updated At fields. ([#2834](https://github.com/stashapp/stash/pull/2834))
|
||||||
|
|||||||
@@ -1060,6 +1060,7 @@
|
|||||||
"submit_update": "Already exists in {endpoint_name}"
|
"submit_update": "Already exists in {endpoint_name}"
|
||||||
},
|
},
|
||||||
"statistics": "Statistics",
|
"statistics": "Statistics",
|
||||||
|
"stash_id_endpoint": "Stash ID Endpoint",
|
||||||
"stats": {
|
"stats": {
|
||||||
"image_size": "Images size",
|
"image_size": "Images size",
|
||||||
"scenes_duration": "Scenes duration",
|
"scenes_duration": "Scenes duration",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
ILabeledValue,
|
ILabeledValue,
|
||||||
INumberValue,
|
INumberValue,
|
||||||
IOptionType,
|
IOptionType,
|
||||||
|
IStashIDValue,
|
||||||
IDateValue,
|
IDateValue,
|
||||||
ITimestampValue,
|
ITimestampValue,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
@@ -29,6 +30,7 @@ export type CriterionValue =
|
|||||||
| ILabeledId[]
|
| ILabeledId[]
|
||||||
| IHierarchicalLabelValue
|
| IHierarchicalLabelValue
|
||||||
| INumberValue
|
| INumberValue
|
||||||
|
| IStashIDValue
|
||||||
| IDateValue
|
| IDateValue
|
||||||
| ITimestampValue;
|
| ITimestampValue;
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { DuplicatedCriterion, PhashCriterionOption } from "./phash";
|
|||||||
import { CaptionCriterion } from "./captions";
|
import { CaptionCriterion } from "./captions";
|
||||||
import { RatingCriterion } from "./rating";
|
import { RatingCriterion } from "./rating";
|
||||||
import { CountryCriterion } from "./country";
|
import { CountryCriterion } from "./country";
|
||||||
|
import { StashIDCriterion } from "./stash-ids";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { IUIConfig } from "src/core/config";
|
import { IUIConfig } from "src/core/config";
|
||||||
import { defaultRatingSystemOptions } from "src/utils/rating";
|
import { defaultRatingSystemOptions } from "src/utils/rating";
|
||||||
@@ -169,6 +170,10 @@ export function makeCriteria(
|
|||||||
return new NumberCriterion(
|
return new NumberCriterion(
|
||||||
new NumberCriterionOption("height", "height_cm", type)
|
new NumberCriterionOption("height", "height_cm", type)
|
||||||
);
|
);
|
||||||
|
// stash_id is deprecated
|
||||||
|
case "stash_id":
|
||||||
|
case "stash_id_endpoint":
|
||||||
|
return new StashIDCriterion();
|
||||||
case "ethnicity":
|
case "ethnicity":
|
||||||
case "hair_color":
|
case "hair_color":
|
||||||
case "eye_color":
|
case "eye_color":
|
||||||
@@ -179,7 +184,6 @@ export function makeCriteria(
|
|||||||
case "piercings":
|
case "piercings":
|
||||||
case "aliases":
|
case "aliases":
|
||||||
case "url":
|
case "url":
|
||||||
case "stash_id":
|
|
||||||
case "details":
|
case "details":
|
||||||
case "title":
|
case "title":
|
||||||
case "director":
|
case "director":
|
||||||
|
|||||||
106
ui/v2.5/src/models/list-filter/criteria/stash-ids.ts
Normal file
106
ui/v2.5/src/models/list-filter/criteria/stash-ids.ts
Normal file
@@ -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<IStashIDValue> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { FavoriteCriterionOption } from "./criteria/favorite";
|
import { FavoriteCriterionOption } from "./criteria/favorite";
|
||||||
import { GenderCriterionOption } from "./criteria/gender";
|
import { GenderCriterionOption } from "./criteria/gender";
|
||||||
import { PerformerIsMissingCriterionOption } from "./criteria/is-missing";
|
import { PerformerIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
|
import { StashIDCriterionOption } from "./criteria/stash-ids";
|
||||||
import { StudiosCriterionOption } from "./criteria/studios";
|
import { StudiosCriterionOption } from "./criteria/studios";
|
||||||
import { TagsCriterionOption } from "./criteria/tags";
|
import { TagsCriterionOption } from "./criteria/tags";
|
||||||
import { ListFilterOptions } from "./filter-options";
|
import { ListFilterOptions } from "./filter-options";
|
||||||
@@ -67,7 +68,6 @@ const stringCriteria: CriterionType[] = [
|
|||||||
"tattoos",
|
"tattoos",
|
||||||
"piercings",
|
"piercings",
|
||||||
"aliases",
|
"aliases",
|
||||||
"stash_id",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
@@ -76,6 +76,7 @@ const criterionOptions = [
|
|||||||
PerformerIsMissingCriterionOption,
|
PerformerIsMissingCriterionOption,
|
||||||
TagsCriterionOption,
|
TagsCriterionOption,
|
||||||
StudiosCriterionOption,
|
StudiosCriterionOption,
|
||||||
|
StashIDCriterionOption,
|
||||||
createStringCriterionOption("url"),
|
createStringCriterionOption("url"),
|
||||||
new NullNumberCriterionOption("rating", "rating100"),
|
new NullNumberCriterionOption("rating", "rating100"),
|
||||||
createMandatoryNumberCriterionOption("tag_count"),
|
createMandatoryNumberCriterionOption("tag_count"),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
} from "./criteria/phash";
|
} from "./criteria/phash";
|
||||||
import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
|
import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
|
||||||
import { CaptionsCriterionOption } from "./criteria/captions";
|
import { CaptionsCriterionOption } from "./criteria/captions";
|
||||||
|
import { StashIDCriterionOption } from "./criteria/stash-ids";
|
||||||
|
|
||||||
const defaultSortBy = "date";
|
const defaultSortBy = "date";
|
||||||
const sortByOptions = [
|
const sortByOptions = [
|
||||||
@@ -82,7 +83,7 @@ const criterionOptions = [
|
|||||||
StudiosCriterionOption,
|
StudiosCriterionOption,
|
||||||
MoviesCriterionOption,
|
MoviesCriterionOption,
|
||||||
createStringCriterionOption("url"),
|
createStringCriterionOption("url"),
|
||||||
createStringCriterionOption("stash_id"),
|
StashIDCriterionOption,
|
||||||
InteractiveCriterionOption,
|
InteractiveCriterionOption,
|
||||||
CaptionsCriterionOption,
|
CaptionsCriterionOption,
|
||||||
createMandatoryNumberCriterionOption("interactive_speed"),
|
createMandatoryNumberCriterionOption("interactive_speed"),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
createMandatoryTimestampCriterionOption,
|
createMandatoryTimestampCriterionOption,
|
||||||
} from "./criteria/criterion";
|
} from "./criteria/criterion";
|
||||||
import { StudioIsMissingCriterionOption } from "./criteria/is-missing";
|
import { StudioIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
|
import { StashIDCriterionOption } from "./criteria/stash-ids";
|
||||||
import { ParentStudiosCriterionOption } from "./criteria/studios";
|
import { ParentStudiosCriterionOption } from "./criteria/studios";
|
||||||
import { ListFilterOptions } from "./filter-options";
|
import { ListFilterOptions } from "./filter-options";
|
||||||
import { DisplayMode } from "./types";
|
import { DisplayMode } from "./types";
|
||||||
@@ -41,7 +42,7 @@ const criterionOptions = [
|
|||||||
createMandatoryNumberCriterionOption("image_count"),
|
createMandatoryNumberCriterionOption("image_count"),
|
||||||
createMandatoryNumberCriterionOption("gallery_count"),
|
createMandatoryNumberCriterionOption("gallery_count"),
|
||||||
createStringCriterionOption("url"),
|
createStringCriterionOption("url"),
|
||||||
createStringCriterionOption("stash_id"),
|
StashIDCriterionOption,
|
||||||
createStringCriterionOption("aliases"),
|
createStringCriterionOption("aliases"),
|
||||||
createMandatoryTimestampCriterionOption("created_at"),
|
createMandatoryTimestampCriterionOption("created_at"),
|
||||||
createMandatoryTimestampCriterionOption("updated_at"),
|
createMandatoryTimestampCriterionOption("updated_at"),
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ export interface IPHashDuplicationValue {
|
|||||||
distance?: number; // currently not implemented
|
distance?: number; // currently not implemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IStashIDValue {
|
||||||
|
endpoint: string;
|
||||||
|
stashID: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IDateValue {
|
export interface IDateValue {
|
||||||
value: string;
|
value: string;
|
||||||
value2: string | undefined;
|
value2: string | undefined;
|
||||||
@@ -57,6 +62,13 @@ export function criterionIsNumberValue(
|
|||||||
return typeof value === "object" && "value" in value && "value2" in value;
|
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(
|
export function criterionIsDateValue(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
value: any
|
value: any
|
||||||
@@ -151,6 +163,7 @@ export type CriterionType =
|
|||||||
| "duplicated"
|
| "duplicated"
|
||||||
| "ignore_auto_tag"
|
| "ignore_auto_tag"
|
||||||
| "file_count"
|
| "file_count"
|
||||||
|
| "stash_id_endpoint"
|
||||||
| "date"
|
| "date"
|
||||||
| "created_at"
|
| "created_at"
|
||||||
| "updated_at"
|
| "updated_at"
|
||||||
|
|||||||
Reference in New Issue
Block a user