mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Merge e057b2899b into 39fd8a6550
This commit is contained in:
commit
2c9c20c1ab
20 changed files with 484 additions and 15 deletions
|
|
@ -81,6 +81,18 @@ input PHashDuplicationCriterionInput {
|
||||||
distance: Int
|
distance: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input StashIDDuplicationCriterionInput {
|
||||||
|
duplicated: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
input TitleDuplicationCriterionInput {
|
||||||
|
duplicated: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
input URLDuplicationCriterionInput {
|
||||||
|
duplicated: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
input StashIDCriterionInput {
|
input StashIDCriterionInput {
|
||||||
"""
|
"""
|
||||||
If present, this value is treated as a predicate.
|
If present, this value is treated as a predicate.
|
||||||
|
|
@ -250,6 +262,12 @@ input SceneFilterType {
|
||||||
o_counter: IntCriterionInput
|
o_counter: IntCriterionInput
|
||||||
"Filter Scenes that have an exact phash match available"
|
"Filter Scenes that have an exact phash match available"
|
||||||
duplicated: PHashDuplicationCriterionInput
|
duplicated: PHashDuplicationCriterionInput
|
||||||
|
"Filter Scenes that have the same stash_id"
|
||||||
|
duplicated_stash_id: StashIDDuplicationCriterionInput
|
||||||
|
"Filter Scenes that have the same title"
|
||||||
|
duplicated_title: TitleDuplicationCriterionInput
|
||||||
|
"Filter Scenes that have the same URL"
|
||||||
|
duplicated_url: URLDuplicationCriterionInput
|
||||||
"Filter by resolution"
|
"Filter by resolution"
|
||||||
resolution: ResolutionCriterionInput
|
resolution: ResolutionCriterionInput
|
||||||
"Filter by orientation"
|
"Filter by orientation"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,18 @@ type PHashDuplicationCriterionInput struct {
|
||||||
Distance *int `json:"distance"`
|
Distance *int `json:"distance"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StashIDDuplicationCriterionInput struct {
|
||||||
|
Duplicated *bool `json:"duplicated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TitleDuplicationCriterionInput struct {
|
||||||
|
Duplicated *bool `json:"duplicated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type URLDuplicationCriterionInput struct {
|
||||||
|
Duplicated *bool `json:"duplicated"`
|
||||||
|
}
|
||||||
|
|
||||||
type SceneFilterType struct {
|
type SceneFilterType struct {
|
||||||
OperatorFilter[SceneFilterType]
|
OperatorFilter[SceneFilterType]
|
||||||
ID *IntCriterionInput `json:"id"`
|
ID *IntCriterionInput `json:"id"`
|
||||||
|
|
@ -35,6 +47,12 @@ type SceneFilterType struct {
|
||||||
OCounter *IntCriterionInput `json:"o_counter"`
|
OCounter *IntCriterionInput `json:"o_counter"`
|
||||||
// Filter Scenes that have an exact phash match available
|
// Filter Scenes that have an exact phash match available
|
||||||
Duplicated *PHashDuplicationCriterionInput `json:"duplicated"`
|
Duplicated *PHashDuplicationCriterionInput `json:"duplicated"`
|
||||||
|
// Filter Scenes that have the same stash_id
|
||||||
|
DuplicatedStashID *StashIDDuplicationCriterionInput `json:"duplicated_stash_id"`
|
||||||
|
// Filter Scenes that have the same title
|
||||||
|
DuplicatedTitle *TitleDuplicationCriterionInput `json:"duplicated_title"`
|
||||||
|
// Filter Scenes that have the same URL
|
||||||
|
DuplicatedURL *URLDuplicationCriterionInput `json:"duplicated_url"`
|
||||||
// Filter by resolution
|
// Filter by resolution
|
||||||
Resolution *ResolutionCriterionInput `json:"resolution"`
|
Resolution *ResolutionCriterionInput `json:"resolution"`
|
||||||
// Filter by orientation
|
// Filter by orientation
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/doug-martin/goqu/v9/exp"
|
"github.com/doug-martin/goqu/v9/exp"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
"gopkg.in/guregu/null.v4"
|
"gopkg.in/guregu/null.v4"
|
||||||
"gopkg.in/guregu/null.v4/zero"
|
"gopkg.in/guregu/null.v4/zero"
|
||||||
)
|
)
|
||||||
|
|
@ -246,8 +247,10 @@ func (qb *GalleryStore) Create(ctx context.Context, newObject *models.Gallery, f
|
||||||
}
|
}
|
||||||
|
|
||||||
if newObject.URLs.Loaded() {
|
if newObject.URLs.Loaded() {
|
||||||
|
urls := newObject.URLs.List()
|
||||||
|
utils.SortURLs(urls)
|
||||||
const startPos = 0
|
const startPos = 0
|
||||||
if err := galleriesURLsTableMgr.insertJoins(ctx, id, startPos, newObject.URLs.List()); err != nil {
|
if err := galleriesURLsTableMgr.insertJoins(ctx, id, startPos, urls); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -286,7 +289,9 @@ func (qb *GalleryStore) Update(ctx context.Context, updatedObject *models.Galler
|
||||||
}
|
}
|
||||||
|
|
||||||
if updatedObject.URLs.Loaded() {
|
if updatedObject.URLs.Loaded() {
|
||||||
if err := galleriesURLsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.URLs.List()); err != nil {
|
urls := updatedObject.URLs.List()
|
||||||
|
utils.SortURLs(urls)
|
||||||
|
if err := galleriesURLsTableMgr.replaceJoins(ctx, updatedObject.ID, urls); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -339,6 +344,15 @@ func (qb *GalleryStore) UpdatePartial(ctx context.Context, id int, partial model
|
||||||
if err := galleriesURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil {
|
if err := galleriesURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Re-sort URLs after modification
|
||||||
|
urls, err := galleriesURLsTableMgr.get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
utils.SortURLs(urls)
|
||||||
|
if err := galleriesURLsTableMgr.replaceJoins(ctx, id, urls); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if partial.PerformerIDs != nil {
|
if partial.PerformerIDs != nil {
|
||||||
if err := galleriesPerformersTableMgr.modifyJoins(ctx, id, partial.PerformerIDs.IDs, partial.PerformerIDs.Mode); err != nil {
|
if err := galleriesPerformersTableMgr.modifyJoins(ctx, id, partial.PerformerIDs.IDs, partial.PerformerIDs.Mode); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"gopkg.in/guregu/null.v4/zero"
|
"gopkg.in/guregu/null.v4/zero"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -172,8 +173,10 @@ func (qb *GroupStore) Create(ctx context.Context, newObject *models.Group) error
|
||||||
}
|
}
|
||||||
|
|
||||||
if newObject.URLs.Loaded() {
|
if newObject.URLs.Loaded() {
|
||||||
|
urls := newObject.URLs.List()
|
||||||
|
utils.SortURLs(urls)
|
||||||
const startPos = 0
|
const startPos = 0
|
||||||
if err := groupsURLsTableMgr.insertJoins(ctx, id, startPos, newObject.URLs.List()); err != nil {
|
if err := groupsURLsTableMgr.insertJoins(ctx, id, startPos, urls); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -219,6 +222,15 @@ func (qb *GroupStore) UpdatePartial(ctx context.Context, id int, partial models.
|
||||||
if err := groupsURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil {
|
if err := groupsURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Re-sort URLs after modification
|
||||||
|
urls, err := groupsURLsTableMgr.get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
utils.SortURLs(urls)
|
||||||
|
if err := groupsURLsTableMgr.replaceJoins(ctx, id, urls); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := qb.tagRelationshipStore.modifyRelationships(ctx, id, partial.TagIDs); err != nil {
|
if err := qb.tagRelationshipStore.modifyRelationships(ctx, id, partial.TagIDs); err != nil {
|
||||||
|
|
@ -245,7 +257,9 @@ func (qb *GroupStore) Update(ctx context.Context, updatedObject *models.Group) e
|
||||||
}
|
}
|
||||||
|
|
||||||
if updatedObject.URLs.Loaded() {
|
if updatedObject.URLs.Loaded() {
|
||||||
if err := groupsURLsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.URLs.List()); err != nil {
|
urls := updatedObject.URLs.List()
|
||||||
|
utils.SortURLs(urls)
|
||||||
|
if err := groupsURLsTableMgr.replaceJoins(ctx, updatedObject.ID, urls); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/sliceutil"
|
"github.com/stashapp/stash/pkg/sliceutil"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
"gopkg.in/guregu/null.v4"
|
"gopkg.in/guregu/null.v4"
|
||||||
"gopkg.in/guregu/null.v4/zero"
|
"gopkg.in/guregu/null.v4/zero"
|
||||||
|
|
||||||
|
|
@ -251,8 +252,10 @@ func (qb *ImageStore) Create(ctx context.Context, newObject *models.Image, fileI
|
||||||
}
|
}
|
||||||
|
|
||||||
if newObject.URLs.Loaded() {
|
if newObject.URLs.Loaded() {
|
||||||
|
urls := newObject.URLs.List()
|
||||||
|
utils.SortURLs(urls)
|
||||||
const startPos = 0
|
const startPos = 0
|
||||||
if err := imagesURLsTableMgr.insertJoins(ctx, id, startPos, newObject.URLs.List()); err != nil {
|
if err := imagesURLsTableMgr.insertJoins(ctx, id, startPos, urls); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -309,6 +312,15 @@ func (qb *ImageStore) UpdatePartial(ctx context.Context, id int, partial models.
|
||||||
if err := imagesURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil {
|
if err := imagesURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Re-sort URLs after modification
|
||||||
|
urls, err := imagesURLsTableMgr.get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
utils.SortURLs(urls)
|
||||||
|
if err := imagesURLsTableMgr.replaceJoins(ctx, id, urls); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if partial.PerformerIDs != nil {
|
if partial.PerformerIDs != nil {
|
||||||
if err := imagesPerformersTableMgr.modifyJoins(ctx, id, partial.PerformerIDs.IDs, partial.PerformerIDs.Mode); err != nil {
|
if err := imagesPerformersTableMgr.modifyJoins(ctx, id, partial.PerformerIDs.IDs, partial.PerformerIDs.Mode); err != nil {
|
||||||
|
|
@ -339,7 +351,9 @@ func (qb *ImageStore) Update(ctx context.Context, updatedObject *models.Image) e
|
||||||
}
|
}
|
||||||
|
|
||||||
if updatedObject.URLs.Loaded() {
|
if updatedObject.URLs.Loaded() {
|
||||||
if err := imagesURLsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.URLs.List()); err != nil {
|
urls := updatedObject.URLs.List()
|
||||||
|
utils.SortURLs(urls)
|
||||||
|
if err := imagesURLsTableMgr.replaceJoins(ctx, updatedObject.ID, urls); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -269,8 +269,10 @@ func (qb *PerformerStore) Create(ctx context.Context, newObject *models.CreatePe
|
||||||
}
|
}
|
||||||
|
|
||||||
if newObject.URLs.Loaded() {
|
if newObject.URLs.Loaded() {
|
||||||
|
urls := newObject.URLs.List()
|
||||||
|
utils.SortURLs(urls)
|
||||||
const startPos = 0
|
const startPos = 0
|
||||||
if err := performersURLsTableMgr.insertJoins(ctx, id, startPos, newObject.URLs.List()); err != nil {
|
if err := performersURLsTableMgr.insertJoins(ctx, id, startPos, urls); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -327,6 +329,15 @@ func (qb *PerformerStore) UpdatePartial(ctx context.Context, id int, partial mod
|
||||||
if err := performersURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil {
|
if err := performersURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Re-sort URLs after modification
|
||||||
|
urls, err := performersURLsTableMgr.get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
utils.SortURLs(urls)
|
||||||
|
if err := performersURLsTableMgr.replaceJoins(ctx, id, urls); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if partial.TagIDs != nil {
|
if partial.TagIDs != nil {
|
||||||
|
|
@ -362,7 +373,9 @@ func (qb *PerformerStore) Update(ctx context.Context, updatedObject *models.Upda
|
||||||
}
|
}
|
||||||
|
|
||||||
if updatedObject.URLs.Loaded() {
|
if updatedObject.URLs.Loaded() {
|
||||||
if err := performersURLsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.URLs.List()); err != nil {
|
urls := updatedObject.URLs.List()
|
||||||
|
utils.SortURLs(urls)
|
||||||
|
if err := performersURLsTableMgr.replaceJoins(ctx, updatedObject.ID, urls); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -445,7 +445,7 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
||||||
url = "url"
|
url = "url"
|
||||||
twitter = "twitter"
|
twitter = "twitter"
|
||||||
instagram = "instagram"
|
instagram = "instagram"
|
||||||
urls = []string{url, twitter, instagram}
|
urls = []string{instagram, twitter, url} // sorted alphabetically
|
||||||
rating = 3
|
rating = 3
|
||||||
ethnicity = "ethnicity"
|
ethnicity = "ethnicity"
|
||||||
country = "country"
|
country = "country"
|
||||||
|
|
|
||||||
|
|
@ -315,8 +315,10 @@ func (qb *SceneStore) Create(ctx context.Context, newObject *models.Scene, fileI
|
||||||
}
|
}
|
||||||
|
|
||||||
if newObject.URLs.Loaded() {
|
if newObject.URLs.Loaded() {
|
||||||
|
urls := newObject.URLs.List()
|
||||||
|
utils.SortURLs(urls)
|
||||||
const startPos = 0
|
const startPos = 0
|
||||||
if err := scenesURLsTableMgr.insertJoins(ctx, id, startPos, newObject.URLs.List()); err != nil {
|
if err := scenesURLsTableMgr.insertJoins(ctx, id, startPos, urls); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -379,6 +381,15 @@ func (qb *SceneStore) UpdatePartial(ctx context.Context, id int, partial models.
|
||||||
if err := scenesURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil {
|
if err := scenesURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Re-sort URLs after modification
|
||||||
|
urls, err := scenesURLsTableMgr.get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
utils.SortURLs(urls)
|
||||||
|
if err := scenesURLsTableMgr.replaceJoins(ctx, id, urls); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if partial.PerformerIDs != nil {
|
if partial.PerformerIDs != nil {
|
||||||
if err := scenesPerformersTableMgr.modifyJoins(ctx, id, partial.PerformerIDs.IDs, partial.PerformerIDs.Mode); err != nil {
|
if err := scenesPerformersTableMgr.modifyJoins(ctx, id, partial.PerformerIDs.IDs, partial.PerformerIDs.Mode); err != nil {
|
||||||
|
|
@ -423,7 +434,9 @@ func (qb *SceneStore) Update(ctx context.Context, updatedObject *models.Scene) e
|
||||||
}
|
}
|
||||||
|
|
||||||
if updatedObject.URLs.Loaded() {
|
if updatedObject.URLs.Loaded() {
|
||||||
if err := scenesURLsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.URLs.List()); err != nil {
|
urls := updatedObject.URLs.List()
|
||||||
|
utils.SortURLs(urls)
|
||||||
|
if err := scenesURLsTableMgr.replaceJoins(ctx, updatedObject.ID, urls); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,9 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
|
||||||
qb.performerFavoriteCriterionHandler(sceneFilter.PerformerFavorite),
|
qb.performerFavoriteCriterionHandler(sceneFilter.PerformerFavorite),
|
||||||
qb.performerAgeCriterionHandler(sceneFilter.PerformerAge),
|
qb.performerAgeCriterionHandler(sceneFilter.PerformerAge),
|
||||||
qb.phashDuplicatedCriterionHandler(sceneFilter.Duplicated, qb.addSceneFilesTable),
|
qb.phashDuplicatedCriterionHandler(sceneFilter.Duplicated, qb.addSceneFilesTable),
|
||||||
|
qb.stashIDDuplicatedCriterionHandler(sceneFilter.DuplicatedStashID),
|
||||||
|
qb.titleDuplicatedCriterionHandler(sceneFilter.DuplicatedTitle),
|
||||||
|
qb.urlDuplicatedCriterionHandler(sceneFilter.DuplicatedURL),
|
||||||
&dateCriterionHandler{sceneFilter.Date, "scenes.date", nil},
|
&dateCriterionHandler{sceneFilter.Date, "scenes.date", nil},
|
||||||
×tampCriterionHandler{sceneFilter.CreatedAt, "scenes.created_at", nil},
|
×tampCriterionHandler{sceneFilter.CreatedAt, "scenes.created_at", nil},
|
||||||
×tampCriterionHandler{sceneFilter.UpdatedAt, "scenes.updated_at", nil},
|
×tampCriterionHandler{sceneFilter.UpdatedAt, "scenes.updated_at", nil},
|
||||||
|
|
@ -297,6 +300,54 @@ func (qb *sceneFilterHandler) phashDuplicatedCriterionHandler(duplicatedFilter *
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *sceneFilterHandler) stashIDDuplicatedCriterionHandler(duplicatedFilter *models.StashIDDuplicationCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
|
if duplicatedFilter != nil && duplicatedFilter.Duplicated != nil {
|
||||||
|
var v string
|
||||||
|
if *duplicatedFilter.Duplicated {
|
||||||
|
v = ">"
|
||||||
|
} else {
|
||||||
|
v = "="
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find stash_ids that appear on more than one scene
|
||||||
|
f.addInnerJoin("(SELECT scene_id FROM scene_stash_ids INNER JOIN (SELECT stash_id FROM scene_stash_ids GROUP BY stash_id HAVING COUNT(DISTINCT scene_id) "+v+" 1) dupes ON scene_stash_ids.stash_id = dupes.stash_id)", "scsi", "scenes.id = scsi.scene_id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *sceneFilterHandler) titleDuplicatedCriterionHandler(duplicatedFilter *models.TitleDuplicationCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
|
if duplicatedFilter != nil && duplicatedFilter.Duplicated != nil {
|
||||||
|
var v string
|
||||||
|
if *duplicatedFilter.Duplicated {
|
||||||
|
v = ">"
|
||||||
|
} else {
|
||||||
|
v = "="
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find titles that appear on more than one scene (excluding empty titles)
|
||||||
|
f.addInnerJoin("(SELECT id FROM scenes WHERE title != '' AND title IS NOT NULL AND title IN (SELECT title FROM scenes WHERE title != '' AND title IS NOT NULL GROUP BY title HAVING COUNT(*) "+v+" 1))", "sctitle", "scenes.id = sctitle.id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *sceneFilterHandler) urlDuplicatedCriterionHandler(duplicatedFilter *models.URLDuplicationCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
|
if duplicatedFilter != nil && duplicatedFilter.Duplicated != nil {
|
||||||
|
var v string
|
||||||
|
if *duplicatedFilter.Duplicated {
|
||||||
|
v = ">"
|
||||||
|
} else {
|
||||||
|
v = "="
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find URLs that appear on more than one scene
|
||||||
|
f.addInnerJoin("(SELECT scene_id FROM scene_urls INNER JOIN (SELECT url FROM scene_urls GROUP BY url HAVING COUNT(DISTINCT scene_id) "+v+" 1) dupes ON scene_urls.url = dupes.url)", "scurl", "scenes.id = scurl.scene_id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *sceneFilterHandler) codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
func (qb *sceneFilterHandler) codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if codec != nil {
|
if codec != nil {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/studio"
|
"github.com/stashapp/stash/pkg/studio"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -191,8 +192,10 @@ func (qb *StudioStore) Create(ctx context.Context, newObject *models.Studio) err
|
||||||
}
|
}
|
||||||
|
|
||||||
if newObject.URLs.Loaded() {
|
if newObject.URLs.Loaded() {
|
||||||
|
urls := newObject.URLs.List()
|
||||||
|
utils.SortURLs(urls)
|
||||||
const startPos = 0
|
const startPos = 0
|
||||||
if err := studiosURLsTableMgr.insertJoins(ctx, id, startPos, newObject.URLs.List()); err != nil {
|
if err := studiosURLsTableMgr.insertJoins(ctx, id, startPos, urls); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -241,6 +244,15 @@ func (qb *StudioStore) UpdatePartial(ctx context.Context, input models.StudioPar
|
||||||
if err := studiosURLsTableMgr.modifyJoins(ctx, input.ID, input.URLs.Values, input.URLs.Mode); err != nil {
|
if err := studiosURLsTableMgr.modifyJoins(ctx, input.ID, input.URLs.Values, input.URLs.Mode); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Re-sort URLs after modification
|
||||||
|
urls, err := studiosURLsTableMgr.get(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
utils.SortURLs(urls)
|
||||||
|
if err := studiosURLsTableMgr.replaceJoins(ctx, input.ID, urls); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := qb.tagRelationshipStore.modifyRelationships(ctx, input.ID, input.TagIDs); err != nil {
|
if err := qb.tagRelationshipStore.modifyRelationships(ctx, input.ID, input.TagIDs); err != nil {
|
||||||
|
|
@ -272,7 +284,9 @@ func (qb *StudioStore) Update(ctx context.Context, updatedObject *models.Studio)
|
||||||
}
|
}
|
||||||
|
|
||||||
if updatedObject.URLs.Loaded() {
|
if updatedObject.URLs.Loaded() {
|
||||||
if err := studiosURLsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.URLs.List()); err != nil {
|
urls := updatedObject.URLs.List()
|
||||||
|
utils.SortURLs(urls)
|
||||||
|
if err := studiosURLsTableMgr.replaceJoins(ctx, updatedObject.ID, urls); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import "regexp"
|
import (
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// URLFromHandle adds the site URL to the input if the input is not already a URL
|
// URLFromHandle adds the site URL to the input if the input is not already a URL
|
||||||
// siteURL must not end with a slash
|
// siteURL must not end with a slash
|
||||||
|
|
@ -13,3 +17,21 @@ func URLFromHandle(input string, siteURL string) string {
|
||||||
|
|
||||||
return siteURL + "/" + input
|
return siteURL + "/" + input
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// urlSortKey extracts the sortable portion of a URL by removing the protocol and www. prefix
|
||||||
|
func urlSortKey(url string) string {
|
||||||
|
// Remove http:// or https://
|
||||||
|
key := strings.TrimPrefix(url, "https://")
|
||||||
|
key = strings.TrimPrefix(key, "http://")
|
||||||
|
// Remove www. prefix
|
||||||
|
key = strings.TrimPrefix(key, "www.")
|
||||||
|
return strings.ToLower(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortURLs sorts a slice of URLs alphabetically by their base URL,
|
||||||
|
// excluding the protocol (http/https) and www. prefix
|
||||||
|
func SortURLs(urls []string) {
|
||||||
|
sort.SliceStable(urls, func(i, j int) bool {
|
||||||
|
return urlSortKey(urls[i]) < urlSortKey(urls[j])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
177
ui/v2.5/src/components/List/Filters/DuplicateFilter.tsx
Normal file
177
ui/v2.5/src/components/List/Filters/DuplicateFilter.tsx
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
import React, { useCallback, useMemo, useState } from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import { BooleanCriterion } from "src/models/list-filter/criteria/criterion";
|
||||||
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
import { Option, SelectedList } from "./SidebarListFilter";
|
||||||
|
import { DuplicatedCriterionOption } from "src/models/list-filter/criteria/phash";
|
||||||
|
import { DuplicatedStashIDCriterionOption } from "src/models/list-filter/criteria/stash-ids";
|
||||||
|
import { DuplicatedTitleCriterionOption } from "src/models/list-filter/criteria/title";
|
||||||
|
import { DuplicatedURLCriterionOption } from "src/models/list-filter/criteria/url";
|
||||||
|
import { SidebarSection } from "src/components/Shared/Sidebar";
|
||||||
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { keyboardClickHandler } from "src/utils/keyboard";
|
||||||
|
|
||||||
|
// Mapping of duplicate type IDs to their criterion options
|
||||||
|
const DUPLICATE_TYPES = {
|
||||||
|
phash: DuplicatedCriterionOption,
|
||||||
|
stash_id: DuplicatedStashIDCriterionOption,
|
||||||
|
title: DuplicatedTitleCriterionOption,
|
||||||
|
url: DuplicatedURLCriterionOption,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type DuplicateTypeId = keyof typeof DUPLICATE_TYPES;
|
||||||
|
|
||||||
|
interface ISidebarDuplicateFilterProps {
|
||||||
|
title?: React.ReactNode;
|
||||||
|
filter: ListFilterModel;
|
||||||
|
setFilter: (f: ListFilterModel) => void;
|
||||||
|
sectionID?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// i18n message IDs for each duplicate type
|
||||||
|
const DUPLICATE_TYPE_MESSAGE_IDS: Record<DuplicateTypeId, string> = {
|
||||||
|
phash: "media_info.phash",
|
||||||
|
stash_id: "stash_id",
|
||||||
|
title: "title",
|
||||||
|
url: "url",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SidebarDuplicateFilter: React.FC<ISidebarDuplicateFilterProps> = ({
|
||||||
|
title,
|
||||||
|
filter,
|
||||||
|
setFilter,
|
||||||
|
sectionID,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [expandedType, setExpandedType] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const trueLabel = intl.formatMessage({ id: "true" });
|
||||||
|
const falseLabel = intl.formatMessage({ id: "false" });
|
||||||
|
|
||||||
|
// Get label for a duplicate type
|
||||||
|
const getLabel = useCallback(
|
||||||
|
(typeId: DuplicateTypeId) =>
|
||||||
|
intl.formatMessage({ id: DUPLICATE_TYPE_MESSAGE_IDS[typeId] }),
|
||||||
|
[intl]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get criterion for a given type
|
||||||
|
const getCriterion = useCallback(
|
||||||
|
(typeId: DuplicateTypeId): BooleanCriterion | null => {
|
||||||
|
const criteria = filter.criteriaFor(
|
||||||
|
DUPLICATE_TYPES[typeId].type
|
||||||
|
) as BooleanCriterion[];
|
||||||
|
return criteria.length > 0 ? criteria[0] : null;
|
||||||
|
},
|
||||||
|
[filter]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build selected items list
|
||||||
|
const selected: Option[] = useMemo(() => {
|
||||||
|
const result: Option[] = [];
|
||||||
|
|
||||||
|
for (const typeId of Object.keys(DUPLICATE_TYPES) as DuplicateTypeId[]) {
|
||||||
|
const criterion = getCriterion(typeId);
|
||||||
|
if (criterion) {
|
||||||
|
const valueLabel = criterion.value === "true" ? trueLabel : falseLabel;
|
||||||
|
result.push({
|
||||||
|
id: typeId,
|
||||||
|
label: `${getLabel(typeId)}: ${valueLabel}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [getCriterion, trueLabel, falseLabel, getLabel]);
|
||||||
|
|
||||||
|
// Available options - show options that aren't already selected
|
||||||
|
const options = useMemo(() => {
|
||||||
|
const result: { id: DuplicateTypeId; label: string }[] = [];
|
||||||
|
|
||||||
|
for (const typeId of Object.keys(DUPLICATE_TYPES) as DuplicateTypeId[]) {
|
||||||
|
if (!getCriterion(typeId)) {
|
||||||
|
result.push({ id: typeId, label: getLabel(typeId) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [getCriterion, getLabel]);
|
||||||
|
|
||||||
|
function onToggleExpand(id: string) {
|
||||||
|
setExpandedType(expandedType === id ? null : id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUnselect(item: Option) {
|
||||||
|
const typeId = item.id as DuplicateTypeId;
|
||||||
|
const criterionOption = DUPLICATE_TYPES[typeId];
|
||||||
|
if (criterionOption) {
|
||||||
|
setFilter(filter.removeCriterion(criterionOption.type));
|
||||||
|
}
|
||||||
|
setExpandedType(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelectValue(typeId: string, value: "true" | "false") {
|
||||||
|
const criterionOption = DUPLICATE_TYPES[typeId as DuplicateTypeId];
|
||||||
|
if (!criterionOption) return;
|
||||||
|
|
||||||
|
const existingCriterion = getCriterion(typeId as DuplicateTypeId);
|
||||||
|
const newCriterion = existingCriterion
|
||||||
|
? existingCriterion.clone()
|
||||||
|
: criterionOption.makeCriterion();
|
||||||
|
newCriterion.value = value;
|
||||||
|
setFilter(filter.replaceCriteria(criterionOption.type, [newCriterion]));
|
||||||
|
setExpandedType(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarSection
|
||||||
|
className="sidebar-list-filter"
|
||||||
|
text={title}
|
||||||
|
sectionID={sectionID}
|
||||||
|
outsideCollapse={
|
||||||
|
<SelectedList items={selected} onUnselect={(i) => onUnselect(i)} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="queryable-candidate-list">
|
||||||
|
<ul>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<React.Fragment key={opt.id}>
|
||||||
|
<li className="unselected-object">
|
||||||
|
<a
|
||||||
|
onClick={() => onToggleExpand(opt.id)}
|
||||||
|
onKeyDown={keyboardClickHandler(() => onToggleExpand(opt.id))}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<div className="label-group">
|
||||||
|
<Icon
|
||||||
|
className="fa-fw include-button single-value"
|
||||||
|
icon={faPlus}
|
||||||
|
/>
|
||||||
|
<span className="unselected-object-label">{opt.label}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{expandedType === opt.id && (
|
||||||
|
<div className="duplicate-sub-options">
|
||||||
|
<div
|
||||||
|
className="duplicate-sub-option"
|
||||||
|
onClick={() => onSelectValue(opt.id, "true")}
|
||||||
|
>
|
||||||
|
{trueLabel}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="duplicate-sub-option"
|
||||||
|
onClick={() => onSelectValue(opt.id, "false")}
|
||||||
|
>
|
||||||
|
{falseLabel}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</SidebarSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -726,6 +726,24 @@ input[type="range"].zoom-slider {
|
||||||
min-height: 2em;
|
min-height: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.duplicate-sub-options {
|
||||||
|
margin-left: 2rem;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
|
.duplicate-sub-option {
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
height: 2em;
|
||||||
|
opacity: 0.8;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(138, 155, 168, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tilted {
|
.tilted {
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ import {
|
||||||
DurationCriterionOption,
|
DurationCriterionOption,
|
||||||
PerformerAgeCriterionOption,
|
PerformerAgeCriterionOption,
|
||||||
} from "src/models/list-filter/scenes";
|
} from "src/models/list-filter/scenes";
|
||||||
|
import { SidebarDuplicateFilter } from "../List/Filters/DuplicateFilter";
|
||||||
import { SidebarAgeFilter } from "../List/Filters/SidebarAgeFilter";
|
import { SidebarAgeFilter } from "../List/Filters/SidebarAgeFilter";
|
||||||
import { SidebarDurationFilter } from "../List/Filters/SidebarDurationFilter";
|
import { SidebarDurationFilter } from "../List/Filters/SidebarDurationFilter";
|
||||||
import {
|
import {
|
||||||
|
|
@ -347,6 +348,12 @@ const SidebarContent: React.FC<{
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
sectionID="organized"
|
sectionID="organized"
|
||||||
/>
|
/>
|
||||||
|
<SidebarDuplicateFilter
|
||||||
|
title={<FormattedMessage id="duplicated" />}
|
||||||
|
filter={filter}
|
||||||
|
setFilter={setFilter}
|
||||||
|
sectionID="duplicated"
|
||||||
|
/>
|
||||||
<SidebarAgeFilter
|
<SidebarAgeFilter
|
||||||
title={<FormattedMessage id="performer_age" />}
|
title={<FormattedMessage id="performer_age" />}
|
||||||
option={PerformerAgeCriterionOption}
|
option={PerformerAgeCriterionOption}
|
||||||
|
|
|
||||||
|
|
@ -1062,7 +1062,11 @@
|
||||||
"select_youngest": "Select the youngest file in the duplicate group",
|
"select_youngest": "Select the youngest file in the duplicate group",
|
||||||
"title": "Duplicate Scenes"
|
"title": "Duplicate Scenes"
|
||||||
},
|
},
|
||||||
|
"duplicated": "Duplicated",
|
||||||
"duplicated_phash": "Duplicated (pHash)",
|
"duplicated_phash": "Duplicated (pHash)",
|
||||||
|
"duplicated_stash_id": "Duplicated (Stash ID)",
|
||||||
|
"duplicated_title": "Duplicated (Title)",
|
||||||
|
"duplicated_url": "Duplicated (URL)",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"effect_filters": {
|
"effect_filters": {
|
||||||
"aspect": "Aspect",
|
"aspect": "Aspect",
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,15 @@ import { IntlShape } from "react-intl";
|
||||||
import {
|
import {
|
||||||
CriterionModifier,
|
CriterionModifier,
|
||||||
StashIdCriterionInput,
|
StashIdCriterionInput,
|
||||||
|
StashIdDuplicationCriterionInput,
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import { IStashIDValue } from "../types";
|
import { IStashIDValue } from "../types";
|
||||||
import {
|
import {
|
||||||
|
BooleanCriterionOption,
|
||||||
ISavedCriterion,
|
ISavedCriterion,
|
||||||
ModifierCriterion,
|
ModifierCriterion,
|
||||||
ModifierCriterionOption,
|
ModifierCriterionOption,
|
||||||
|
StringCriterion,
|
||||||
} from "./criterion";
|
} from "./criterion";
|
||||||
|
|
||||||
export const StashIDCriterionOption = new ModifierCriterionOption({
|
export const StashIDCriterionOption = new ModifierCriterionOption({
|
||||||
|
|
@ -145,3 +148,21 @@ export class StashIDCriterion extends ModifierCriterion<IStashIDValue> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DuplicatedStashIDCriterionOption = new BooleanCriterionOption(
|
||||||
|
"duplicated_stash_id",
|
||||||
|
"duplicated_stash_id",
|
||||||
|
() => new DuplicatedStashIDCriterion()
|
||||||
|
);
|
||||||
|
|
||||||
|
export class DuplicatedStashIDCriterion extends StringCriterion {
|
||||||
|
constructor() {
|
||||||
|
super(DuplicatedStashIDCriterionOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
public toCriterionInput(): StashIdDuplicationCriterionInput {
|
||||||
|
return {
|
||||||
|
duplicated: this.value === "true",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
20
ui/v2.5/src/models/list-filter/criteria/title.ts
Normal file
20
ui/v2.5/src/models/list-filter/criteria/title.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { TitleDuplicationCriterionInput } from "src/core/generated-graphql";
|
||||||
|
import { BooleanCriterionOption, StringCriterion } from "./criterion";
|
||||||
|
|
||||||
|
export const DuplicatedTitleCriterionOption = new BooleanCriterionOption(
|
||||||
|
"duplicated_title",
|
||||||
|
"duplicated_title",
|
||||||
|
() => new DuplicatedTitleCriterion()
|
||||||
|
);
|
||||||
|
|
||||||
|
export class DuplicatedTitleCriterion extends StringCriterion {
|
||||||
|
constructor() {
|
||||||
|
super(DuplicatedTitleCriterionOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
public toCriterionInput(): TitleDuplicationCriterionInput {
|
||||||
|
return {
|
||||||
|
duplicated: this.value === "true",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
20
ui/v2.5/src/models/list-filter/criteria/url.ts
Normal file
20
ui/v2.5/src/models/list-filter/criteria/url.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { UrlDuplicationCriterionInput } from "src/core/generated-graphql";
|
||||||
|
import { BooleanCriterionOption, StringCriterion } from "./criterion";
|
||||||
|
|
||||||
|
export const DuplicatedURLCriterionOption = new BooleanCriterionOption(
|
||||||
|
"duplicated_url",
|
||||||
|
"duplicated_url",
|
||||||
|
() => new DuplicatedURLCriterion()
|
||||||
|
);
|
||||||
|
|
||||||
|
export class DuplicatedURLCriterion extends StringCriterion {
|
||||||
|
constructor() {
|
||||||
|
super(DuplicatedURLCriterionOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
public toCriterionInput(): UrlDuplicationCriterionInput {
|
||||||
|
return {
|
||||||
|
duplicated: this.value === "true",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,7 +31,12 @@ 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";
|
import {
|
||||||
|
DuplicatedStashIDCriterionOption,
|
||||||
|
StashIDCriterionOption,
|
||||||
|
} from "./criteria/stash-ids";
|
||||||
|
import { DuplicatedTitleCriterionOption } from "./criteria/title";
|
||||||
|
import { DuplicatedURLCriterionOption } from "./criteria/url";
|
||||||
import { RatingCriterionOption } from "./criteria/rating";
|
import { RatingCriterionOption } from "./criteria/rating";
|
||||||
import { PathCriterionOption } from "./criteria/path";
|
import { PathCriterionOption } from "./criteria/path";
|
||||||
import { OrientationCriterionOption } from "./criteria/orientation";
|
import { OrientationCriterionOption } from "./criteria/orientation";
|
||||||
|
|
@ -100,6 +105,9 @@ const criterionOptions = [
|
||||||
createStringCriterionOption("checksum", "media_info.checksum"),
|
createStringCriterionOption("checksum", "media_info.checksum"),
|
||||||
PhashCriterionOption,
|
PhashCriterionOption,
|
||||||
DuplicatedCriterionOption,
|
DuplicatedCriterionOption,
|
||||||
|
DuplicatedStashIDCriterionOption,
|
||||||
|
DuplicatedTitleCriterionOption,
|
||||||
|
DuplicatedURLCriterionOption,
|
||||||
OrganizedCriterionOption,
|
OrganizedCriterionOption,
|
||||||
RatingCriterionOption,
|
RatingCriterionOption,
|
||||||
createMandatoryNumberCriterionOption("o_counter", "o_count", {
|
createMandatoryNumberCriterionOption("o_counter", "o_count", {
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,9 @@ export type CriterionType =
|
||||||
| "favorite"
|
| "favorite"
|
||||||
| "performer_age"
|
| "performer_age"
|
||||||
| "duplicated"
|
| "duplicated"
|
||||||
|
| "duplicated_stash_id"
|
||||||
|
| "duplicated_title"
|
||||||
|
| "duplicated_url"
|
||||||
| "ignore_auto_tag"
|
| "ignore_auto_tag"
|
||||||
| "file_count"
|
| "file_count"
|
||||||
| "stash_id_endpoint"
|
| "stash_id_endpoint"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue