Saved filter refactor (#4054)

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
yoshnopa 2023-09-01 02:04:56 +02:00 committed by GitHub
parent fca162f1ca
commit 20520a58b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 1062 additions and 657 deletions

View file

@ -118,4 +118,6 @@ models:
model: github.com/stashapp/stash/internal/identify.MetadataOptions
ScraperSourceInput:
model: github.com/stashapp/stash/pkg/scraper.Source
SavedFindFilterType:
model: github.com/stashapp/stash/pkg/models.FindFilterType

View file

@ -2,5 +2,13 @@ fragment SavedFilterData on SavedFilter {
id
mode
name
filter
find_filter {
q
page
per_page
sort
direction
}
object_filter
ui_options
}

View file

@ -12,6 +12,17 @@ input FindFilterType {
direction: SortDirectionEnum
}
type SavedFindFilterType {
q: String
page: Int
"""
use per_page = -1 to indicate all results. Defaults to 25.
"""
per_page: Int
sort: String
direction: SortDirectionEnum
}
enum ResolutionEnum {
"144p"
VERY_LOW
@ -604,6 +615,13 @@ type SavedFilter {
name: String!
"JSON-encoded filter string"
filter: String!
@deprecated(reason: "use find_filter and object_filter instead")
find_filter: SavedFindFilterType
# maps to any of the AnyFilterInput types
# using a generic Map instead of creating and maintaining match types for inputs
object_filter: Map
# generic map for ui options
ui_options: Map
}
input SaveFilterInput {
@ -611,8 +629,10 @@ input SaveFilterInput {
id: ID
mode: FilterMode!
name: String!
"JSON-encoded filter string"
filter: String!
find_filter: FindFilterType
object_filter: Map
# generic map for ui options
ui_options: Map
}
input DestroyFilterInput {
@ -621,6 +641,9 @@ input DestroyFilterInput {
input SetDefaultFilterInput {
mode: FilterMode!
"JSON-encoded filter string - null to clear"
filter: String
"null to clear"
find_filter: FindFilterType
object_filter: Map
# generic map for ui options
ui_options: Map
}

View file

@ -82,6 +82,9 @@ func (r *Resolver) Subscription() SubscriptionResolver {
func (r *Resolver) Tag() TagResolver {
return &tagResolver{r}
}
func (r *Resolver) SavedFilter() SavedFilterResolver {
return &savedFilterResolver{r}
}
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
@ -96,6 +99,7 @@ type imageResolver struct{ *Resolver }
type studioResolver struct{ *Resolver }
type movieResolver struct{ *Resolver }
type tagResolver struct{ *Resolver }
type savedFilterResolver struct{ *Resolver }
func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
return txn.WithTxn(ctx, r.txnManager, fn)

View file

@ -0,0 +1,11 @@
package api
import (
"context"
"github.com/stashapp/stash/pkg/models"
)
func (r *savedFilterResolver) Filter(ctx context.Context, obj *models.SavedFilter) (string, error) {
return "", nil
}

View file

@ -14,12 +14,6 @@ func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput
return nil, errors.New("name must be non-empty")
}
newFilter := models.SavedFilter{
Mode: input.Mode,
Name: input.Name,
Filter: input.Filter,
}
var id *int
if input.ID != nil {
idv, err := strconv.Atoi(*input.ID)
@ -32,17 +26,27 @@ func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.SavedFilter
if id == nil {
err = qb.Create(ctx, &newFilter)
} else {
newFilter.ID = *id
err = qb.Update(ctx, &newFilter)
f := models.SavedFilter{
Mode: input.Mode,
Name: input.Name,
FindFilter: input.FindFilter,
ObjectFilter: input.ObjectFilter,
UIOptions: input.UIOptions,
}
if id == nil {
err = qb.Create(ctx, &f)
ret = &f
} else {
f.ID = *id
err = qb.Update(ctx, &f)
ret = &f
}
return err
}); err != nil {
return nil, err
}
ret = &newFilter
return ret, err
}
@ -65,7 +69,7 @@ func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input SetDefaul
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.SavedFilter
if input.Filter == nil {
if input.FindFilter == nil && input.ObjectFilter == nil && input.UIOptions == nil {
// clearing
def, err := qb.FindDefault(ctx, input.Mode)
if err != nil {
@ -79,12 +83,12 @@ func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input SetDefaul
return nil
}
err := qb.SetDefault(ctx, &models.SavedFilter{
Mode: input.Mode,
Filter: *input.Filter,
return qb.SetDefault(ctx, &models.SavedFilter{
Mode: input.Mode,
FindFilter: input.FindFilter,
ObjectFilter: input.ObjectFilter,
UIOptions: input.UIOptions,
})
return err
}); err != nil {
return false, err
}

View file

@ -99,7 +99,7 @@ func createPerformer(ctx context.Context, pqb models.PerformerWriter) error {
func createStudio(ctx context.Context, qb models.StudioWriter, name string) (*models.Studio, error) {
// create the studio
studio := models.Studio{
Name: name,
Name: name,
}
err := qb.Create(ctx, &studio)

View file

@ -60,11 +60,12 @@ func (e FilterMode) MarshalGQL(w io.Writer) {
}
type SavedFilter struct {
ID int `json:"id"`
Mode FilterMode `json:"mode"`
Name string `json:"name"`
// JSON-encoded filter string
Filter string `json:"filter"`
ID int `db:"id" json:"id"`
Mode FilterMode `db:"mode" json:"mode"`
Name string `db:"name" json:"name"`
FindFilter *FindFilterType `json:"find_filter"`
ObjectFilter map[string]interface{} `json:"object_filter"`
UIOptions map[string]interface{} `json:"ui_options"`
}
type SavedFilters []*SavedFilter

View file

@ -33,7 +33,7 @@ const (
dbConnTimeout = 30
)
var appSchemaVersion uint = 48
var appSchemaVersion uint = 49
//go:embed migrations/*.sql
var migrationsBox embed.FS
@ -74,10 +74,10 @@ type Database struct {
Scene *SceneStore
SceneMarker *SceneMarkerStore
Performer *PerformerStore
SavedFilter *SavedFilterStore
Studio *StudioStore
Tag *TagStore
Movie *MovieStore
SavedFilter *SavedFilterStore
db *sqlx.DB
dbPath string

View file

@ -0,0 +1,417 @@
package migrations
import (
"context"
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sqlite"
)
var migrate49TypeResolution = map[string][]string{
"Boolean": {
/*
"organized",
"interactive",
"ignore_auto_tag",
"performer_favorite",
"filter_favorites",
*/
},
"Int": {
"id",
"rating",
"rating100",
"o_counter",
"duration",
"tag_count",
"age",
"height",
"height_cm",
"weight",
"scene_count",
"marker_count",
"image_count",
"gallery_count",
"performer_count",
"interactive_speed",
"resume_time",
"play_count",
"play_duration",
"parent_count",
"child_count",
"performer_age",
"file_count",
},
"Float": {
"penis_length",
},
"Object": {
"tags",
"performers",
"studios",
"movies",
"galleries",
"parents",
"children",
"scene_tags",
"performer_tags",
},
}
var migrate49NameChanges = map[string]string{
"rating": "rating100",
"parent_studios": "parents",
"child_studios": "children",
"parent_tags": "parents",
"child_tags": "children",
"child_tag_count": "child_count",
"parent_tag_count": "parent_count",
"height": "height_cm",
"imageIsMissing": "is_missing",
"sceneIsMissing": "is_missing",
"galleryIsMissing": "is_missing",
"performerIsMissing": "is_missing",
"tagIsMissing": "is_missing",
"studioIsMissing": "is_missing",
"movieIsMissing": "is_missing",
"favorite": "filter_favorites",
"hasMarkers": "has_markers",
"parentTags": "parents",
"childTags": "children",
"phash": "phash_distance",
"scene_code": "code",
"hasChapters": "has_chapters",
"sceneChecksum": "checksum",
"galleryChecksum": "checksum",
"sceneTags": "scene_tags",
"performerTags": "performer_tags",
}
func post49(ctx context.Context, db *sqlx.DB) error {
logger.Info("Running post-migration for schema version 49")
m := schema49Migrator{
migrator: migrator{
db: db,
},
}
return m.migrateSavedFilters(ctx)
}
type schema49Migrator struct {
migrator
}
func (m *schema49Migrator) migrateSavedFilters(ctx context.Context) error {
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
rows, err := m.db.Query("SELECT id, mode, find_filter FROM saved_filters ORDER BY id")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
id int
mode models.FilterMode
findFilter string
)
err := rows.Scan(&id, &mode, &findFilter)
if err != nil {
return err
}
asRawMessage := json.RawMessage(findFilter)
newFindFilter, err := m.getFindFilter(asRawMessage)
if err != nil {
return fmt.Errorf("failed to get find filter for saved filter %d: %w", id, err)
}
objectFilter, err := m.getObjectFilter(mode, asRawMessage)
if err != nil {
return fmt.Errorf("failed to get object filter for saved filter %d: %w", id, err)
}
uiOptions, err := m.getDisplayOptions(asRawMessage)
if err != nil {
return fmt.Errorf("failed to get display options for saved filter %d: %w", id, err)
}
_, err = m.db.Exec("UPDATE saved_filters SET find_filter = ?, object_filter = ?, ui_options = ? WHERE id = ?", newFindFilter, objectFilter, uiOptions, id)
if err != nil {
return fmt.Errorf("failed to update saved filter %d: %w", id, err)
}
}
return rows.Err()
}); err != nil {
return err
}
return nil
}
func (m *schema49Migrator) getDisplayOptions(data json.RawMessage) (json.RawMessage, error) {
type displayOptions struct {
DisplayMode *int `json:"disp"`
ZoomIndex *int `json:"z"`
}
var opts displayOptions
if err := json.Unmarshal(data, &opts); err != nil {
return nil, fmt.Errorf("failed to unmarshal display options: %w", err)
}
ret := make(map[string]interface{})
if opts.DisplayMode != nil {
ret["display_mode"] = *opts.DisplayMode
}
if opts.ZoomIndex != nil {
ret["zoom_index"] = *opts.ZoomIndex
}
return json.Marshal(ret)
}
func (m *schema49Migrator) getFindFilter(data json.RawMessage) (json.RawMessage, error) {
type findFilterJson struct {
Q *string `json:"q"`
Page *int `json:"page"`
PerPage *int `json:"perPage"`
Sort *string `json:"sortby"`
Direction *string `json:"sortdir"`
}
ppDefault := 40
pageDefault := 1
qDefault := ""
sortDefault := "date"
asc := "asc"
ff := findFilterJson{Q: &qDefault, Page: &pageDefault, PerPage: &ppDefault, Sort: &sortDefault, Direction: &asc}
if err := json.Unmarshal(data, &ff); err != nil {
return nil, fmt.Errorf("failed to unmarshal find filter: %w", err)
}
newDir := strings.ToUpper(*ff.Direction)
ff.Direction = &newDir
type findFilterRewrite struct {
Q *string `json:"q"`
Page *int `json:"page"`
PerPage *int `json:"per_page"`
Sort *string `json:"sort"`
Direction *string `json:"direction"`
}
fr := findFilterRewrite(ff)
return json.Marshal(fr)
}
func (m *schema49Migrator) getObjectFilter(mode models.FilterMode, data json.RawMessage) (json.RawMessage, error) {
type criteriaJson struct {
Criteria []string `json:"c"`
}
var c criteriaJson
if err := json.Unmarshal(data, &c); err != nil {
return nil, fmt.Errorf("failed to unmarshal object filter: %w", err)
}
ret := make(map[string]interface{})
for _, raw := range c.Criteria {
if err := m.convertCriterion(mode, ret, raw); err != nil {
return nil, err
}
}
return json.Marshal(ret)
}
func (m *schema49Migrator) convertCriterion(mode models.FilterMode, out map[string]interface{}, criterion string) error {
// convert to a map
ret := make(map[string]interface{})
if err := json.Unmarshal([]byte(criterion), &ret); err != nil {
return fmt.Errorf("failed to unmarshal criterion: %w", err)
}
field := ret["type"].(string)
// Some names are deprecated
if newFieldName, ok := migrate49NameChanges[field]; ok {
field = newFieldName
}
delete(ret, "type")
// Find out whether the object needs some adjustment/has non-string content attached
// Only adjust if value is present
if v, ok := ret["value"]; ok && v != nil {
var err error
switch {
case arrayContains(migrate49TypeResolution["Boolean"], field):
ret["value"], err = m.adjustCriterionValue(ret["value"], "bool")
case arrayContains(migrate49TypeResolution["Int"], field):
ret["value"], err = m.adjustCriterionValue(ret["value"], "int")
case arrayContains(migrate49TypeResolution["Float"], field):
ret["value"], err = m.adjustCriterionValue(ret["value"], "float64")
case arrayContains(migrate49TypeResolution["Object"], field):
ret["value"], err = m.adjustCriterionValue(ret["value"], "object")
}
if err != nil {
return fmt.Errorf("failed to adjust criterion value for %q: %w", field, err)
}
}
out[field] = ret
return nil
}
func arrayContains(sl []string, name string) bool {
for _, value := range sl {
if value == name {
return true
}
}
return false
}
// General Function for converting the types inside a criterion
func (m *schema49Migrator) adjustCriterionValue(value interface{}, typ string) (interface{}, error) {
if mapvalue, ok := value.(map[string]interface{}); ok {
// Primitive values and lists of them
var err error
for _, next := range []string{"value", "value2"} {
if valmap, ok := mapvalue[next].([]string); ok {
var valNewMap []interface{}
for index, v := range valmap {
valNewMap[index], err = m.convertValue(v, typ)
if err != nil {
return nil, err
}
}
mapvalue[next] = valNewMap
} else if _, ok := mapvalue[next]; ok {
mapvalue[next], err = m.convertValue(mapvalue[next], typ)
if err != nil {
return nil, err
}
}
}
// Items
for _, next := range []string{"items", "excluded"} {
if _, ok := mapvalue[next]; ok {
mapvalue[next], err = m.adjustCriterionItem(mapvalue[next])
if err != nil {
return nil, err
}
}
}
// Those Values are always Int
for _, next := range []string{"Distance", "Depth"} {
if _, ok := mapvalue[next]; ok {
mapvalue[next], err = strconv.ParseInt(mapvalue[next].(string), 10, 64)
if err != nil {
return nil, err
}
}
}
return mapvalue, nil
} else if _, ok := value.(string); ok {
// Singular Primitive Values
return m.convertValue(value, typ)
} else if listvalue, ok := value.([]interface{}); ok {
// Items as a singular value, as well as singular lists
var err error
if typ == "object" {
value, err = m.adjustCriterionItem(value)
if err != nil {
return nil, err
}
} else {
for index, val := range listvalue {
listvalue[index], err = m.convertValue(val, typ)
if err != nil {
return nil, err
}
}
value = listvalue
}
return value, nil
} else if _, ok := value.(int); ok {
return value, nil
}
return nil, fmt.Errorf("could not recognize format of value %v", value)
}
// Converts values inside a criterion that represent some objects, like performer or studio.
func (m *schema49Migrator) adjustCriterionItem(value interface{}) (interface{}, error) {
// Basically, this first converts step by step the value, after that it adjusts id and Depth (of parent/child studios) to int
if itemlist, ok := value.([]interface{}); ok {
var itemNewList []interface{}
for _, val := range itemlist {
if val, ok := val.(map[string]interface{}); ok {
newItem := make(map[string]interface{})
for index, v := range val {
if v, ok := v.(string); ok {
switch index {
case "id":
if formattedOut, ok := strconv.ParseInt(v, 10, 64); ok == nil {
newItem["id"] = formattedOut
}
case "Depth":
if formattedOut, ok := strconv.ParseInt(v, 10, 64); ok == nil {
newItem["Depth"] = formattedOut
}
default:
newItem[index] = v
}
}
}
itemNewList = append(itemNewList, newItem)
}
}
return itemNewList, nil
}
return nil, fmt.Errorf("could not recognize %v as an item list", value)
}
// Converts a value of type string to its according type, given by string
func (m *schema49Migrator) convertValue(value interface{}, typ string) (interface{}, error) {
valueType := reflect.TypeOf(value).Name()
if typ == valueType || (typ == "int" && valueType == "float64") || (typ == "float64" && valueType == "int") {
return value, nil
}
if val, ok := value.(string); ok {
switch typ {
case "float64":
return strconv.ParseFloat(val, 64)
case "int":
return strconv.ParseInt(val, 10, 64)
case "bool":
return strconv.ParseBool(val)
default:
return nil, fmt.Errorf("no valid conversion type for %v, need bool, int or float64", typ)
}
}
return nil, fmt.Errorf("cannot convert %v (%T) to %s", value, value, typ)
}
func init() {
sqlite.RegisterPostMigration(49, post49)
}

View file

@ -0,0 +1,34 @@
PRAGMA foreign_keys=OFF;
-- remove filter column
CREATE TABLE `saved_filters_new` (
`id` integer not null primary key autoincrement,
`name` varchar(510) not null,
`mode` varchar(255) not null,
`find_filter` blob,
`object_filter` blob,
`ui_options` blob
);
-- move filter data into find_filter to be migrated in the post-migration
INSERT INTO `saved_filters_new`
(
`id`,
`name`,
`mode`,
`find_filter`
)
SELECT
`id`,
`name`,
`mode`,
`filter`
FROM `saved_filters`;
DROP INDEX `index_saved_filters_on_mode_name_unique`;
DROP TABLE `saved_filters`;
ALTER TABLE `saved_filters_new` rename to `saved_filters`;
CREATE UNIQUE INDEX `index_saved_filters_on_mode_name_unique` on `saved_filters` (`mode`, `name`);
PRAGMA foreign_keys=ON;

View file

@ -291,7 +291,7 @@ func TestMovieUpdateFrontImage(t *testing.T) {
// create movie to test against
const name = "TestMovieUpdateMovieImages"
movie := models.Movie{
Name: name,
Name: name,
}
err := qb.Create(ctx, &movie)
if err != nil {
@ -311,7 +311,7 @@ func TestMovieUpdateBackImage(t *testing.T) {
// create movie to test against
const name = "TestMovieUpdateMovieImages"
movie := models.Movie{
Name: name,
Name: name,
}
err := qb.Create(ctx, &movie)
if err != nil {

View file

@ -3,6 +3,7 @@ package sqlite
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
@ -10,6 +11,7 @@ import (
"github.com/doug-martin/goqu/v9/exp"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
)
@ -20,25 +22,67 @@ const (
)
type savedFilterRow struct {
ID int `db:"id" goqu:"skipinsert"`
Mode string `db:"mode"`
Name string `db:"name"`
Filter string `db:"filter"`
ID int `db:"id" goqu:"skipinsert"`
Mode models.FilterMode `db:"mode"`
Name string `db:"name"`
FindFilter string `db:"find_filter"`
ObjectFilter string `db:"object_filter"`
UIOptions string `db:"ui_options"`
}
func encodeJSONOrEmpty(v interface{}) string {
if v == nil {
return ""
}
encoded, err := json.Marshal(v)
if err != nil {
logger.Errorf("error encoding json %v: %v", v, err)
}
return string(encoded)
}
func decodeJSON(s string, v interface{}) {
if s == "" {
return
}
if err := json.Unmarshal([]byte(s), v); err != nil {
logger.Errorf("error decoding json %q: %v", s, err)
}
}
func (r *savedFilterRow) fromSavedFilter(o models.SavedFilter) {
r.ID = o.ID
r.Mode = string(o.Mode)
r.Mode = o.Mode
r.Name = o.Name
r.Filter = o.Filter
// encode the filters as json
r.FindFilter = encodeJSONOrEmpty(o.FindFilter)
r.ObjectFilter = encodeJSONOrEmpty(o.ObjectFilter)
r.UIOptions = encodeJSONOrEmpty(o.UIOptions)
}
func (r *savedFilterRow) resolve() *models.SavedFilter {
ret := &models.SavedFilter{
ID: r.ID,
Name: r.Name,
Mode: models.FilterMode(r.Mode),
Filter: r.Filter,
ID: r.ID,
Mode: r.Mode,
Name: r.Name,
}
// decode the filters from json
if r.FindFilter != "" {
ret.FindFilter = &models.FindFilterType{}
decodeJSON(r.FindFilter, &ret.FindFilter)
}
if r.ObjectFilter != "" {
ret.ObjectFilter = make(map[string]interface{})
decodeJSON(r.ObjectFilter, &ret.ObjectFilter)
}
if r.UIOptions != "" {
ret.UIOptions = make(map[string]interface{})
decodeJSON(r.UIOptions, &ret.UIOptions)
}
return ret
@ -46,7 +90,6 @@ func (r *savedFilterRow) resolve() *models.SavedFilter {
type SavedFilterStore struct {
repository
tableMgr *table
}
@ -77,7 +120,7 @@ func (qb *SavedFilterStore) Create(ctx context.Context, newObject *models.SavedF
return err
}
updated, err := qb.find(ctx, id)
updated, err := qb.Find(ctx, id)
if err != nil {
return fmt.Errorf("finding after create: %w", err)
}
@ -166,7 +209,6 @@ func (qb *SavedFilterStore) find(ctx context.Context, id int) (*models.SavedFilt
return ret, nil
}
// returns nil, sql.ErrNoRows if not found
func (qb *SavedFilterStore) get(ctx context.Context, q *goqu.SelectDataset) (*models.SavedFilter, error) {
ret, err := qb.getMany(ctx, q)
if err != nil {

View file

@ -42,15 +42,35 @@ func TestSavedFilterFindByMode(t *testing.T) {
func TestSavedFilterDestroy(t *testing.T) {
const filterName = "filterToDestroy"
const testFilter = "{}"
filterQ := ""
filterPage := 1
filterPerPage := 40
filterSort := "date"
filterDirection := models.SortDirectionEnumAsc
findFilter := models.FindFilterType{
Q: &filterQ,
Page: &filterPage,
PerPage: &filterPerPage,
Sort: &filterSort,
Direction: &filterDirection,
}
objectFilter := map[string]interface{}{
"test": "foo",
}
uiOptions := map[string]interface{}{
"display_mode": 1,
"zoom_index": 1,
}
var id int
// create the saved filter to destroy
withTxn(func(ctx context.Context) error {
newFilter := models.SavedFilter{
Name: filterName,
Mode: models.FilterModeScenes,
Filter: testFilter,
Name: filterName,
Mode: models.FilterModeScenes,
FindFilter: &findFilter,
ObjectFilter: objectFilter,
UIOptions: uiOptions,
}
err := db.SavedFilter.Create(ctx, &newFilter)
@ -88,12 +108,32 @@ func TestSavedFilterFindDefault(t *testing.T) {
}
func TestSavedFilterSetDefault(t *testing.T) {
const newFilter = "foo"
filterQ := ""
filterPage := 1
filterPerPage := 40
filterSort := "date"
filterDirection := models.SortDirectionEnumAsc
findFilter := models.FindFilterType{
Q: &filterQ,
Page: &filterPage,
PerPage: &filterPerPage,
Sort: &filterSort,
Direction: &filterDirection,
}
objectFilter := map[string]interface{}{
"test": "foo",
}
uiOptions := map[string]interface{}{
"display_mode": 1,
"zoom_index": 1,
}
withTxn(func(ctx context.Context) error {
err := db.SavedFilter.SetDefault(ctx, &models.SavedFilter{
Mode: models.FilterModeMovies,
Filter: newFilter,
Mode: models.FilterModeMovies,
FindFilter: &findFilter,
ObjectFilter: objectFilter,
UIOptions: uiOptions,
})
return err
@ -104,7 +144,7 @@ func TestSavedFilterSetDefault(t *testing.T) {
def, err := db.SavedFilter.FindDefault(ctx, models.FilterModeMovies)
if err == nil {
defID = def.ID
assert.Equal(t, newFilter, def.Filter)
assert.Equal(t, &findFilter, def.FindFilter)
}
return err

View file

@ -1714,10 +1714,29 @@ func getSavedFilterName(index int) string {
func createSavedFilters(ctx context.Context, qb models.SavedFilterReaderWriter, n int) error {
for i := 0; i < n; i++ {
filterQ := ""
filterPage := i
filterPerPage := i * 40
filterSort := "date"
filterDirection := models.SortDirectionEnumAsc
findFilter := models.FindFilterType{
Q: &filterQ,
Page: &filterPage,
PerPage: &filterPerPage,
Sort: &filterSort,
Direction: &filterDirection,
}
savedFilter := models.SavedFilter{
Mode: getSavedFilterMode(i),
Name: getSavedFilterName(i),
Filter: getPrefixedStringValue("savedFilter", i, "Filter"),
Mode: getSavedFilterMode(i),
Name: getSavedFilterName(i),
FindFilter: &findFilter,
ObjectFilter: map[string]interface{}{
"test": "object",
},
UIOptions: map[string]interface{}{
"display_mode": 1,
"zoom_index": 1,
},
}
err := qb.Create(ctx, &savedFilter)

View file

@ -74,7 +74,7 @@
"prefer-destructuring": ["error", { "object": true, "array": false }],
"@typescript-eslint/no-use-before-define": [
"error",
{ "functions": false, "classes": true }
{ "functions": false, "classes": false }
],
"no-nested-ternary": "off"
}

View file

@ -105,11 +105,11 @@ const SavedFilterResults: React.FC<ISavedFilterResults> = ({
const filter = useMemo(() => {
if (!data?.findSavedFilter) return;
const { mode, filter: filterJSON } = data.findSavedFilter;
const { mode } = data.findSavedFilter;
const ret = new ListFilterModel(mode, config);
ret.currentPage = 1;
ret.configureFromJSON(filterJSON);
ret.configureFromSavedFilter(data.findSavedFilter);
ret.randomSeed = -1;
return ret;
}, [data?.findSavedFilter, config]);

View file

@ -270,11 +270,11 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
if (existing) {
setCriterion(existing);
} else {
const newCriterion = makeCriteria(configuration, option.type);
const newCriterion = makeCriteria(filter.mode, option.type);
setCriterion(newCriterion);
}
},
[criteria, configuration]
[filter.mode, criteria]
);
const ui = (configuration?.ui ?? {}) as IUIConfig;

View file

@ -13,20 +13,19 @@ interface IHierarchicalLabelValueFilterProps {
export const HierarchicalLabelValueFilter: React.FC<
IHierarchicalLabelValueFilterProps
> = ({ criterion, onValueChanged }) => {
const { criterionOption } = criterion;
const { type, inputType } = criterionOption;
const intl = useIntl();
if (
criterion.criterionOption.type !== "performers" &&
criterion.criterionOption.type !== "studios" &&
criterion.criterionOption.type !== "parent_studios" &&
criterion.criterionOption.type !== "tags" &&
criterion.criterionOption.type !== "sceneTags" &&
criterion.criterionOption.type !== "performerTags" &&
criterion.criterionOption.type !== "parentTags" &&
criterion.criterionOption.type !== "childTags" &&
criterion.criterionOption.type !== "movies"
)
inputType !== "studios" &&
inputType !== "tags" &&
inputType !== "scene_tags" &&
inputType !== "performer_tags"
) {
return null;
}
const messages = defineMessages({
studio_depth: {
@ -51,10 +50,10 @@ export const HierarchicalLabelValueFilter: React.FC<
}
function criterionOptionTypeToIncludeID(): string {
if (criterion.criterionOption.type === "studios") {
if (inputType === "studios") {
return "include-sub-studios";
}
if (criterion.criterionOption.type === "childTags") {
if (type === "children") {
return "include-parent-tags";
}
return "include-sub-tags";
@ -62,9 +61,9 @@ export const HierarchicalLabelValueFilter: React.FC<
function criterionOptionTypeToIncludeUIString(): MessageDescriptor {
const optionType =
criterion.criterionOption.type === "studios"
inputType === "studios"
? "include_sub_studios"
: criterion.criterionOption.type === "childTags"
: type === "children"
? "include_parent_tags"
: "include_sub_tags";
return {
@ -76,7 +75,7 @@ export const HierarchicalLabelValueFilter: React.FC<
<>
<Form.Group>
<FilterSelect
type={criterion.criterionOption.type}
type={inputType}
isMulti
onSelect={onSelectionChanged}
ids={criterion.value.items.map((labeled) => labeled.id)}

View file

@ -13,18 +13,19 @@ export const LabeledIdFilter: React.FC<ILabeledIdFilterProps> = ({
criterion,
onValueChanged,
}) => {
const { criterionOption } = criterion;
const { inputType } = criterionOption;
if (
criterion.criterionOption.type !== "performers" &&
criterion.criterionOption.type !== "studios" &&
criterion.criterionOption.type !== "parent_studios" &&
criterion.criterionOption.type !== "tags" &&
criterion.criterionOption.type !== "sceneTags" &&
criterion.criterionOption.type !== "performerTags" &&
criterion.criterionOption.type !== "parentTags" &&
criterion.criterionOption.type !== "childTags" &&
criterion.criterionOption.type !== "movies"
)
inputType !== "performers" &&
inputType !== "studios" &&
inputType !== "scene_tags" &&
inputType !== "performer_tags" &&
inputType !== "tags" &&
inputType !== "movies"
) {
return null;
}
function onSelectionChanged(items: SelectObject[]) {
onValueChanged(
@ -38,7 +39,7 @@ export const LabeledIdFilter: React.FC<ILabeledIdFilterProps> = ({
return (
<Form.Group>
<FilterSelect
type={criterion.criterionOption.type}
type={inputType}
isMulti
onSelect={onSelectionChanged}
ids={criterion.value.map((labeled) => labeled.id)}

View file

@ -320,7 +320,7 @@ export const HierarchicalObjectsFilter = <
if (criterion.criterionOption.type === "studios") {
return "include-sub-studios";
}
if (criterion.criterionOption.type === "childTags") {
if (criterion.criterionOption.type === "children") {
return "include-parent-tags";
}
return "include-sub-tags";
@ -330,7 +330,7 @@ export const HierarchicalObjectsFilter = <
const optionType =
criterion.criterionOption.type === "studios"
? "include_sub_studios"
: criterion.criterionOption.type === "childTags"
: criterion.criterionOption.type === "children"
? "include_parent_tags"
: "include_sub_tags";
return {

View file

@ -619,8 +619,8 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
if (defaultFilter?.findDefaultFilter) {
newFilter.currentPage = 1;
try {
newFilter.configureFromJSON(
defaultFilter.findDefaultFilter.filter
newFilter.configureFromSavedFilter(
defaultFilter.findDefaultFilter
);
} catch (err) {
console.log(err);

View file

@ -75,7 +75,9 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
id,
mode: filter.mode,
name,
filter: filterCopy.makeSavedFilterJSON(),
find_filter: filterCopy.makeFindFilter(),
object_filter: filterCopy.makeSavedFindFilter(),
ui_options: filterCopy.makeUIOptions(),
},
},
});
@ -143,7 +145,9 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
variables: {
input: {
mode: filter.mode,
filter: filterCopy.makeSavedFilterJSON(),
find_filter: filterCopy.makeFindFilter(),
object_filter: filterCopy.makeSavedFindFilter(),
ui_options: filterCopy.makeUIOptions(),
},
},
});
@ -166,7 +170,7 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
newFilter.currentPage = 1;
// #1795 - reset search term if not present in saved filter
newFilter.searchTerm = "";
newFilter.configureFromJSON(f.filter);
newFilter.configureFromSavedFilter(f);
// #1507 - reset random seed when loaded
newFilter.randomSeed = -1;

View file

@ -44,12 +44,9 @@ interface ITypeProps {
type?:
| "performers"
| "studios"
| "parent_studios"
| "tags"
| "sceneTags"
| "performerTags"
| "parentTags"
| "childTags"
| "scene_tags"
| "performer_tags"
| "movies";
}
interface IFilterProps {
@ -865,7 +862,7 @@ export const TagSelect: React.FC<
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) => {
if (props.type === "performers") {
return <PerformerSelect {...props} creatable={false} />;
} else if (props.type === "studios" || props.type === "parent_studios") {
} else if (props.type === "studios") {
return <StudioSelect {...props} creatable={false} />;
} else if (props.type === "movies") {
return <MovieSelect {...props} creatable={false} />;

View file

@ -17,7 +17,7 @@ export const StudioChildrenPanel: React.FC<IStudioChildrenPanel> = ({
const studioValue = { id: studio.id!, label: studio.name! };
// if studio is already present, then we modify it, otherwise add
let parentStudioCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "parent_studios";
return c.criterionOption.type === "parents";
}) as ParentStudiosCriterion;
if (

View file

@ -877,7 +877,7 @@
"path": "Sti",
"perceptual_similarity": "Perceptuel lighed (phash)",
"performer": "Kunstner",
"performerTags": "Kunstner Tags",
"performer_tags": "Kunstner Tags",
"performer_age": "kunstnere Alder",
"performer_count": "Kunstner Antal",
"performer_favorite": "Foretrukken optrædende",
@ -927,7 +927,7 @@
"resolution": "Opløsning",
"scene": "Scene",
"sceneTagger": "Scenetagger",
"sceneTags": "Scene-etiketter",
"scene_tags": "Scene-etiketter",
"scene_count": "Scene antal",
"scene_id": "Scene-id",
"scenes": "Scener",

View file

@ -1033,7 +1033,7 @@
"penis_length_cm": "Penislänge (cm)",
"perceptual_similarity": "Wahrnehmungsähnlichkeit (phash)",
"performer": "Darsteller",
"performerTags": "Darsteller-Tags",
"performer_tags": "Darsteller-Tags",
"performer_age": "Alter der Darsteller",
"performer_count": "Darstelleranzahl",
"performer_favorite": "Darsteller favorisiert",
@ -1088,7 +1088,7 @@
"resume_time": "Zeit fortsetzen",
"scene": "Szene",
"sceneTagger": "Szenen-Tagger",
"sceneTags": "Szenen-Tags",
"scene_tags": "Szenen-Tags",
"scene_code": "Studio Code",
"scene_count": "Szenenanzahl",
"scene_created_at": "Szene angelegt am",

View file

@ -1075,7 +1075,7 @@
"penis_length_cm": "Penis Length (cm)",
"perceptual_similarity": "Perceptual Similarity (phash)",
"performer": "Performer",
"performerTags": "Performer Tags",
"performer_tags": "Performer Tags",
"performer_age": "Performer Age",
"performer_count": "Performer Count",
"performer_favorite": "Performer Favourited",
@ -1130,7 +1130,7 @@
"resume_time": "Resume Time",
"scene": "Scene",
"sceneTagger": "Scene Tagger",
"sceneTags": "Scene Tags",
"scene_tags": "Scene Tags",
"scene_code": "Studio Code",
"scene_count": "Scene Count",
"scene_created_at": "Scene Created At",

View file

@ -929,7 +929,7 @@
"path": "Ruta",
"perceptual_similarity": "Similaridad perceptiva (phash)",
"performer": "Actriz/Actor",
"performerTags": "Etiquetas de actriz/actor",
"performer_tags": "Etiquetas de actriz/actor",
"performer_age": "Edad de la actriz/actor",
"performer_count": "Número de actrices/actores",
"performer_favorite": "Actriz/actor favorita/o",
@ -977,7 +977,7 @@
"resolution": "Resolución",
"scene": "Escena",
"sceneTagger": "Etiquetador de escenas",
"sceneTags": "Etiquetas de escena",
"scene_tags": "Etiquetas de escena",
"scene_count": "Número de escenas",
"scene_id": "Indentificador de escena",
"scenes": "Escenas",

View file

@ -1012,7 +1012,7 @@
"path": "Failitee",
"perceptual_similarity": "Tajutav Sarnasus (phash)",
"performer": "Näitleja",
"performerTags": "Näitleja Sildid",
"performer_tags": "Näitleja Sildid",
"performer_age": "Näitleja Vanus",
"performer_count": "Näitlejate Arv",
"performer_favorite": "Lemmiknäitleja",
@ -1067,7 +1067,7 @@
"resume_time": "Jätkamisaeg",
"scene": "Stseen",
"sceneTagger": "Stseeni Sildistaja",
"sceneTags": "Stseeni Sildid",
"scene_tags": "Stseeni Sildid",
"scene_code": "Stuudio Kood",
"scene_count": "Stseenide Arv",
"scene_created_at": "Stseen Loodud",

View file

@ -870,7 +870,7 @@
"path": "Polku",
"perceptual_similarity": "Aistinvarainen samankaltaisuus (phash)",
"performer": "Esiintyjä",
"performerTags": "Esiintyjien tunnisteet",
"performer_tags": "Esiintyjien tunnisteet",
"performer_age": "Esiintyjän ikä",
"performer_count": "Esiintyjien määrä",
"performer_favorite": "Esiintyjä suosikeissa",
@ -914,7 +914,7 @@
"resolution": "Resoluutio",
"scene": "Kohtaus",
"sceneTagger": "Kohtauksien tunnistetila",
"sceneTags": "Kohtauksen tunnisteet",
"scene_tags": "Kohtauksen tunnisteet",
"scene_code": "Studiokoodi",
"scene_count": "Kohtauksien määrä",
"scene_created_at": "Kohtaus luotu",

View file

@ -1072,7 +1072,7 @@
"penis_length_cm": "Longueur du pénis (cm)",
"perceptual_similarity": "Similitude perceptuelle (empreinte)",
"performer": "Performeurs",
"performerTags": "Étiquettes de performeur",
"performer_tags": "Étiquettes de performeur",
"performer_age": "Âge du performeur",
"performer_count": "Nombre de performeurs",
"performer_favorite": "Performeur favori",
@ -1127,7 +1127,7 @@
"resume_time": "Reprendre le temps",
"scene": "Scène",
"sceneTagger": "Étiqueteuse de scènes",
"sceneTags": "Étiquettes de la scène",
"scene_tags": "Étiquettes de la scène",
"scene_code": "Code studio",
"scene_count": "Nombre de scènes",
"scene_created_at": "Scène créée le",

View file

@ -437,7 +437,7 @@
"parent_tags": "Szülő-címkék",
"path": "Elérési Út",
"performer": "Szereplő",
"performerTags": "Szereplő Címkék",
"performer_tags": "Szereplő Címkék",
"performer_age": "Szereplő Kora",
"performer_count": "Szereplők Száma",
"performer_favorite": "Szereplő Kedvencek Közt",
@ -460,7 +460,7 @@
"resolution": "Felbontás",
"scene": "Jelenet",
"sceneTagger": "Jelenetcímkéző",
"sceneTags": "Jelenetcímkék",
"scene_tags": "Jelenetcímkék",
"scene_count": "Jelenetszám",
"scene_id": "Jelenet ID",
"scenes": "Jelenetek",

View file

@ -986,7 +986,7 @@
"resume_time": "Tempo Continuazione",
"scene": "Scena",
"sceneTagger": "Tagger Scena",
"sceneTags": "Tag Scena",
"scene_tags": "Tag Scena",
"scene_code": "Codice dello Studio",
"scene_count": "Numero Scene",
"scene_created_at": "Scena Creata Al",

View file

@ -940,7 +940,7 @@
"path": "パス",
"perceptual_similarity": "知覚的類似性 (phash)",
"performer": "出演者",
"performerTags": "出演者タグ",
"performer_tags": "出演者タグ",
"performer_age": "出演者の年齢",
"performer_count": "出演者数",
"performer_favorite": "出演者をお気に入り済み",
@ -995,7 +995,7 @@
"resume_time": "レジューム時間",
"scene": "シーン",
"sceneTagger": "シーン一括タグ付け",
"sceneTags": "シーンタグ",
"scene_tags": "シーンタグ",
"scene_code": "スタジオコード",
"scene_count": "シーン数",
"scene_created_at": "シーンの作成日時",

View file

@ -1022,7 +1022,7 @@
"penis_length_cm": "자지 크기 (cm)",
"perceptual_similarity": "유사도 (phash)",
"performer": "배우",
"performerTags": "배우 태그",
"performer_tags": "배우 태그",
"performer_age": "배우 나이",
"performer_count": "배우 수",
"performer_favorite": "즐겨찾기한 배우",
@ -1077,7 +1077,7 @@
"resume_time": "재시작 시간",
"scene": "영상",
"sceneTagger": "영상 태거",
"sceneTags": "영상 태그",
"scene_tags": "영상 태그",
"scene_code": "스튜디오 코드",
"scene_count": "영상 개수",
"scene_created_at": "영상 생성 날짜",

View file

@ -821,7 +821,7 @@
"path": "Pad",
"perceptual_similarity": "Perceptuele gelijkenis (phash)",
"performer": "Performer",
"performerTags": "Peformer Labels",
"performer_tags": "Peformer Labels",
"performer_age": "Leeftijd artiest",
"performer_count": "Performer Aantal",
"performer_favorite": "Artiest favoriet",
@ -855,7 +855,7 @@
"resolution": "Resolutie",
"scene": "Scène",
"sceneTagger": "Scene Labelen",
"sceneTags": "Scene Labels",
"scene_tags": "Scene Labels",
"scene_count": "Scene Aantal",
"scene_id": "Scene ID",
"scenes": "Scènes",

View file

@ -1030,7 +1030,7 @@
"penis_length_cm": "Długość penisa (cm)",
"perceptual_similarity": "Podobieństwo percepcyjne (phash)",
"performer": "Aktor",
"performerTags": "Tagi aktorów",
"performer_tags": "Tagi aktorów",
"performer_age": "Wiek aktora",
"performer_count": "Liczba aktorów",
"performer_favorite": "Ulubiony aktor",
@ -1085,7 +1085,7 @@
"resume_time": "Rozpocznij od",
"scene": "Scena",
"sceneTagger": "Otagowywacz scen",
"sceneTags": "Tagi sceny",
"scene_tags": "Tagi sceny",
"scene_code": "Kod studia",
"scene_count": "Liczba scen",
"scene_created_at": "Scena utworzona",

View file

@ -847,7 +847,7 @@
"path": "Caminho",
"perceptual_similarity": "Semelhança Perceptiva (phash)",
"performer": "Artista",
"performerTags": "Etiquetas de artistas",
"performer_tags": "Etiquetas de artistas",
"performer_age": "Idade do Artista",
"performer_count": "Contagem de artistas",
"performer_favorite": "Artista Favoritado",
@ -898,7 +898,7 @@
"resolution": "Resolução",
"scene": "Cena",
"sceneTagger": "Etiquetador de cena",
"sceneTags": "Etiquetas da cena",
"scene_tags": "Etiquetas da cena",
"scene_count": "Contagem de cena",
"scene_id": "Cena ID",
"scenes": "Cenas",

View file

@ -933,7 +933,7 @@
"path": "Путь",
"perceptual_similarity": "Воспринимаемое сходство (phash)",
"performer": "Актер",
"performerTags": "Теги актера",
"performer_tags": "Теги актера",
"performer_age": "Возраст актера",
"performer_count": "Количество актеров",
"performer_favorite": "Участник добавлен в избранное",
@ -988,7 +988,7 @@
"resume_time": "Таймкод воспроизведения",
"scene": "Сцена",
"sceneTagger": "Пометка сцен тэгами",
"sceneTags": "Тэги сцен",
"scene_tags": "Тэги сцен",
"scene_code": "Идентификатор сцены",
"scene_count": "Количество сцен",
"scene_created_at": "Сцена создана",

View file

@ -1033,7 +1033,7 @@
"penis_length_cm": "Penislängd (cm)",
"perceptual_similarity": "Perceptuell likhet (phash)",
"performer": "Stjärna",
"performerTags": "Stjärntagg",
"performer_tags": "Stjärntagg",
"performer_age": "Ålder på stjärna",
"performer_count": "Antal stjärnor",
"performer_favorite": "Favoritiserad stjärna",
@ -1088,7 +1088,7 @@
"resume_time": "Återupptagningstid",
"scene": "Scen",
"sceneTagger": "Scentaggaren",
"sceneTags": "Scentaggar",
"scene_tags": "Scentaggar",
"scene_code": "Studiokod",
"scene_count": "Antal scener",
"scene_created_at": "Scenen Skapad",

View file

@ -747,7 +747,7 @@
"part_of": "{parent} öğesinin parçası",
"path": "Konum",
"performer": "Oyuncu",
"performerTags": "Oyuncu Etiketleri",
"performer_tags": "Oyuncu Etiketleri",
"performer_count": "Oyuncu Sayısı",
"performer_image": "Oyuncu Resmi",
"performers": "Oyuncular",
@ -758,7 +758,7 @@
"resolution": "Çözünürlük",
"scene": "Sahne",
"sceneTagger": "Sahne Etiketleyici",
"sceneTags": "Sahne Etiketleri",
"scene_tags": "Sahne Etiketleri",
"scene_count": "Sahne Sayısı",
"scene_id": "Sahne Kimliği (ID)",
"scenes": "Sahneler",

View file

@ -1018,7 +1018,7 @@
"path": "路径",
"perceptual_similarity": "感知的类似程度(感知码)",
"performer": "演员",
"performerTags": "演员标签",
"performer_tags": "演员标签",
"performer_age": "演员年龄",
"performer_count": "演员数量",
"performer_favorite": "演员已收藏",
@ -1073,7 +1073,7 @@
"resume_time": "恢复时间",
"scene": "短片",
"sceneTagger": "短片标记器",
"sceneTags": "短片标记",
"scene_tags": "短片标记",
"scene_code": "工作室代码",
"scene_count": "短片数量",
"scene_created_at": "短片建立在",

View file

@ -956,7 +956,7 @@
"path": "路徑",
"perceptual_similarity": "感知相似度 (PHash)",
"performer": "演員",
"performerTags": "演員標籤",
"performer_tags": "演員標籤",
"performer_age": "演員年齡",
"performer_count": "演員數量",
"performer_favorite": "已收藏的演員",
@ -1011,7 +1011,7 @@
"resume_time": "恢復播放時間",
"scene": "短片",
"sceneTagger": "短片標籤器",
"sceneTags": "短片標籤",
"scene_tags": "短片標籤",
"scene_code": "番號",
"scene_count": "短片數量",
"scene_created_at": "短片建立於",

View file

@ -10,7 +10,6 @@ class CaptionsCriterionOptionType extends CriterionOption {
super({
messageID: value,
type: value,
parameterName: value,
modifierOptions: [
CriterionModifier.Includes,
CriterionModifier.Excludes,
@ -19,6 +18,7 @@ class CaptionsCriterionOptionType extends CriterionOption {
],
defaultModifier: CriterionModifier.Includes,
options: languageStrings,
makeCriterion: () => new CaptionCriterion(),
});
}
}

View file

@ -16,6 +16,7 @@ export const CircumcisedCriterionOption = new CriterionOption({
CriterionModifier.IsNull,
CriterionModifier.NotNull,
],
makeCriterion: () => new CircumcisedCriterion(),
});
export class CircumcisedCriterion extends MultiStringCriterion {

View file

@ -3,11 +3,7 @@ import { CriterionModifier } from "src/core/generated-graphql";
import { getCountryByISO } from "src/utils/country";
import { StringCriterion, StringCriterionOption } from "./criterion";
const countryCriterionOption = new StringCriterionOption(
"country",
"country",
"country"
);
const countryCriterionOption = new StringCriterionOption("country", "country");
export class CountryCriterion extends StringCriterion {
constructor() {

View file

@ -121,7 +121,7 @@ export abstract class Criterion<V extends CriterionValue> {
}
public getId(): string {
return `${this.criterionOption.parameterName}-${this.modifier.toString()}`; // TODO add values?
return `${this.criterionOption.type}-${this.modifier.toString()}`; // TODO add values?
}
public toJSON() {
@ -154,7 +154,7 @@ export abstract class Criterion<V extends CriterionValue> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public apply(outputFilter: Record<string, any>) {
// eslint-disable-next-line no-param-reassign
outputFilter[this.criterionOption.parameterName] = this.toCriterionInput();
outputFilter[this.criterionOption.type] = this.toCriterionInput();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -164,50 +164,68 @@ export abstract class Criterion<V extends CriterionValue> {
modifier: this.modifier,
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toSavedFilter(outputFilter: Record<string, any>) {
outputFilter[this.criterionOption.type] = {
value: this.value,
modifier: this.modifier,
};
}
}
export type InputType = "number" | "text" | undefined;
export type InputType =
| "number"
| "text"
| "performers"
| "studios"
| "tags"
| "performer_tags"
| "scene_tags"
| "movies"
| "galleries"
| undefined;
interface ICriterionOptionsParams {
messageID: string;
type: CriterionType;
inputType?: InputType;
parameterName?: string;
modifierOptions?: CriterionModifier[];
defaultModifier?: CriterionModifier;
options?: Option[];
makeCriterion: () => Criterion<CriterionValue>;
}
export class CriterionOption {
public readonly messageID: string;
public readonly type: CriterionType;
public readonly parameterName: string;
public readonly modifierOptions: CriterionModifier[];
public readonly defaultModifier: CriterionModifier;
public readonly options: Option[] | undefined;
public readonly inputType: InputType;
public readonly makeCriterionFn: (
o: CriterionOption
) => Criterion<CriterionValue>;
constructor(options: ICriterionOptionsParams) {
this.messageID = options.messageID;
this.type = options.type;
this.parameterName = options.parameterName ?? options.type;
this.modifierOptions = options.modifierOptions ?? [];
this.defaultModifier = options.defaultModifier ?? CriterionModifier.Equals;
this.options = options.options;
this.inputType = options.inputType;
this.makeCriterionFn = options.makeCriterion;
}
public makeCriterion() {
return this.makeCriterionFn(this);
}
}
export class StringCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
constructor(messageID: string, type: CriterionType, options?: Option[]) {
super({
messageID,
type: value,
parameterName,
type,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
@ -221,20 +239,16 @@ export class StringCriterionOption extends CriterionOption {
defaultModifier: CriterionModifier.Equals,
options,
inputType: "text",
makeCriterion: () => new StringCriterion(this),
});
}
}
export function createStringCriterionOption(
value: CriterionType,
messageID?: string,
parameterName?: string
type: CriterionType,
messageID?: string
) {
return new StringCriterionOption(
messageID ?? value,
value,
parameterName ?? messageID ?? value
);
return new StringCriterionOption(messageID ?? type, type);
}
export class StringCriterion extends Criterion<string> {
@ -274,16 +288,10 @@ export class MultiStringCriterion extends Criterion<string[]> {
}
export class MandatoryStringCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
constructor(messageID: string, value: CriterionType, options?: Option[]) {
super({
messageID,
type: value,
parameterName,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
@ -295,45 +303,42 @@ export class MandatoryStringCriterionOption extends CriterionOption {
defaultModifier: CriterionModifier.Equals,
options,
inputType: "text",
makeCriterion: () => new StringCriterion(this),
});
}
}
export function createMandatoryStringCriterionOption(
value: CriterionType,
messageID?: string,
parameterName?: string
messageID?: string
) {
return new MandatoryStringCriterionOption(
messageID ?? value,
value,
parameterName ?? messageID ?? value
);
return new MandatoryStringCriterionOption(messageID ?? value, value);
}
export class PathCriterionOption extends StringCriterionOption {}
export function createPathCriterionOption(
value: CriterionType,
messageID?: string,
parameterName?: string
type: CriterionType,
messageID?: string
) {
return new PathCriterionOption(
messageID ?? value,
value,
parameterName ?? messageID ?? value
);
return new PathCriterionOption(messageID ?? type, type);
}
export class BooleanCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType, parameterName?: string) {
constructor(
messageID: string,
value: CriterionType,
makeCriterion?: () => Criterion<CriterionValue>
) {
super({
messageID,
type: value,
parameterName,
modifierOptions: [],
defaultModifier: CriterionModifier.Equals,
options: [true.toString(), false.toString()],
makeCriterion: makeCriterion
? makeCriterion
: () => new BooleanCriterion(this),
});
}
}
@ -350,27 +355,16 @@ export class BooleanCriterion extends StringCriterion {
export function createBooleanCriterionOption(
value: CriterionType,
messageID?: string,
parameterName?: string
messageID?: string
) {
return new BooleanCriterionOption(
messageID ?? value,
value,
parameterName ?? messageID ?? value
);
return new BooleanCriterionOption(messageID ?? value, value);
}
export class NumberCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
constructor(messageID: string, value: CriterionType, options?: Option[]) {
super({
messageID,
type: value,
parameterName,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
@ -384,16 +378,16 @@ export class NumberCriterionOption extends CriterionOption {
defaultModifier: CriterionModifier.Equals,
options,
inputType: "number",
makeCriterion: () => new NumberCriterion(this),
});
}
}
export class NullNumberCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType, parameterName?: string) {
constructor(messageID: string, value: CriterionType) {
super({
messageID,
type: value,
parameterName,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
@ -406,16 +400,17 @@ export class NullNumberCriterionOption extends CriterionOption {
],
defaultModifier: CriterionModifier.Equals,
inputType: "number",
makeCriterion: () => new NumberCriterion(this),
});
}
}
export function createNumberCriterionOption(value: CriterionType) {
return new NumberCriterionOption(value, value, value);
return new NumberCriterionOption(value, value);
}
export function createNullNumberCriterionOption(value: CriterionType) {
return new NullNumberCriterionOption(value, value, value);
return new NullNumberCriterionOption(value, value);
}
export class NumberCriterion extends Criterion<INumberValue> {
@ -437,8 +432,8 @@ export class NumberCriterion extends Criterion<INumberValue> {
protected toCriterionInput(): IntCriterionInput {
return {
modifier: this.modifier,
value: this.value.value ?? 0,
value2: this.value.value2,
value: this.value?.value ?? 0,
value2: this.value?.value2,
};
}
@ -487,8 +482,8 @@ export class ILabeledIdCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
parameterName: string,
includeAll: boolean
includeAll: boolean,
inputType: InputType
) {
const modifierOptions = [
CriterionModifier.Includes,
@ -506,9 +501,10 @@ export class ILabeledIdCriterionOption extends CriterionOption {
super({
messageID,
type: value,
parameterName,
modifierOptions,
defaultModifier,
makeCriterion: () => new ILabeledIdCriterion(this),
inputType,
});
}
}
@ -684,11 +680,10 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
}
export class MandatoryNumberCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType, parameterName?: string) {
constructor(messageID: string, value: CriterionType) {
super({
messageID,
type: value,
parameterName,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
@ -699,6 +694,7 @@ export class MandatoryNumberCriterionOption extends CriterionOption {
],
defaultModifier: CriterionModifier.Equals,
inputType: "number",
makeCriterion: () => new NumberCriterion(this),
});
}
}
@ -707,7 +703,7 @@ export function createMandatoryNumberCriterionOption(
value: CriterionType,
messageID?: string
) {
return new MandatoryNumberCriterionOption(messageID ?? value, value, value);
return new MandatoryNumberCriterionOption(messageID ?? value, value);
}
export class DurationCriterion extends Criterion<INumberValue> {
@ -718,8 +714,8 @@ export class DurationCriterion extends Criterion<INumberValue> {
protected toCriterionInput(): IntCriterionInput {
return {
modifier: this.modifier,
value: this.value.value ?? 0,
value2: this.value.value2,
value: this.value?.value ?? 0,
value2: this.value?.value2,
};
}
@ -771,16 +767,10 @@ export class PhashDuplicateCriterion extends StringCriterion {
}
export class DateCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
constructor(messageID: string, value: CriterionType, options?: Option[]) {
super({
messageID,
type: value,
parameterName,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
@ -794,12 +784,13 @@ export class DateCriterionOption extends CriterionOption {
defaultModifier: CriterionModifier.Equals,
options,
inputType: "text",
makeCriterion: () => new DateCriterion(this),
});
}
}
export function createDateCriterionOption(value: CriterionType) {
return new DateCriterionOption(value, value, value);
return new DateCriterionOption(value, value);
}
export class DateCriterion extends Criterion<IDateValue> {
@ -813,8 +804,8 @@ export class DateCriterion extends Criterion<IDateValue> {
protected toCriterionInput(): DateCriterionInput {
return {
modifier: this.modifier,
value: this.value.value,
value2: this.value.value2,
value: this.value?.value,
value2: this.value?.value2,
};
}
@ -856,16 +847,10 @@ export class DateCriterion extends Criterion<IDateValue> {
}
export class TimestampCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
constructor(messageID: string, value: CriterionType, options?: Option[]) {
super({
messageID,
type: value,
parameterName,
modifierOptions: [
CriterionModifier.GreaterThan,
CriterionModifier.LessThan,
@ -877,19 +862,20 @@ export class TimestampCriterionOption extends CriterionOption {
defaultModifier: CriterionModifier.GreaterThan,
options,
inputType: "text",
makeCriterion: () => new TimestampCriterion(this),
});
}
}
export function createTimestampCriterionOption(value: CriterionType) {
return new TimestampCriterionOption(value, value, value);
return new TimestampCriterionOption(value, value);
}
export class TimestampCriterion extends Criterion<ITimestampValue> {
public encodeValue() {
return {
value: this.value.value,
value2: this.value.value2,
value: this.value?.value,
value2: this.value?.value2,
};
}
@ -950,16 +936,10 @@ export class TimestampCriterion extends Criterion<ITimestampValue> {
}
export class MandatoryTimestampCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
constructor(messageID: string, value: CriterionType, options?: Option[]) {
super({
messageID,
type: value,
parameterName,
modifierOptions: [
CriterionModifier.GreaterThan,
CriterionModifier.LessThan,
@ -969,10 +949,11 @@ export class MandatoryTimestampCriterionOption extends CriterionOption {
defaultModifier: CriterionModifier.GreaterThan,
options,
inputType: "text",
makeCriterion: () => new TimestampCriterion(this),
});
}
}
export function createMandatoryTimestampCriterionOption(value: CriterionType) {
return new MandatoryTimestampCriterionOption(value, value, value);
return new MandatoryTimestampCriterionOption(value, value);
}

View file

@ -1,250 +1,36 @@
/* eslint-disable consistent-return, default-case */
import {
StringCriterion,
NumberCriterion,
DurationCriterion,
NumberCriterionOption,
MandatoryStringCriterionOption,
NullNumberCriterionOption,
MandatoryNumberCriterionOption,
StringCriterionOption,
ILabeledIdCriterion,
BooleanCriterion,
BooleanCriterionOption,
DateCriterion,
DateCriterionOption,
TimestampCriterion,
MandatoryTimestampCriterionOption,
PathCriterionOption,
} from "./criterion";
import { OrganizedCriterion } from "./organized";
import { FavoriteCriterion, PerformerFavoriteCriterion } from "./favorite";
import { HasMarkersCriterion } from "./has-markers";
import { HasChaptersCriterion } from "./has-chapters";
import {
PerformerIsMissingCriterionOption,
ImageIsMissingCriterionOption,
TagIsMissingCriterionOption,
SceneIsMissingCriterionOption,
IsMissingCriterion,
GalleryIsMissingCriterionOption,
StudioIsMissingCriterionOption,
MovieIsMissingCriterionOption,
} from "./is-missing";
import { NoneCriterion } from "./none";
import { PerformersCriterion } from "./performers";
import { AverageResolutionCriterion, ResolutionCriterion } from "./resolution";
import { StudiosCriterion, ParentStudiosCriterion } from "./studios";
import {
ChildTagsCriterionOption,
ParentTagsCriterionOption,
PerformerTagsCriterionOption,
SceneTagsCriterionOption,
TagsCriterion,
TagsCriterionOption,
} from "./tags";
import { GenderCriterion } from "./gender";
import { CircumcisedCriterion } from "./circumcised";
import { MoviesCriterionOption } from "./movies";
import { GalleriesCriterion } from "./galleries";
import { CriterionType } from "../types";
import { InteractiveCriterion } from "./interactive";
import { DuplicatedCriterion, PhashCriterion } from "./phash";
import { CaptionCriterion } from "./captions";
import { RatingCriterion } from "./rating";
import { CountryCriterion } from "./country";
import { StashIDCriterion } from "./stash-ids";
import * as GQL from "src/core/generated-graphql";
import { IUIConfig } from "src/core/config";
import { defaultRatingSystemOptions } from "src/utils/rating";
import { SceneListFilterOptions } from "../scenes";
import { MovieListFilterOptions } from "../movies";
import { GalleryListFilterOptions } from "../galleries";
import { PerformerListFilterOptions } from "../performers";
import { ImageListFilterOptions } from "../images";
import { SceneMarkerListFilterOptions } from "../scene-markers";
import { StudioListFilterOptions } from "../studios";
import { TagListFilterOptions } from "../tags";
import { CriterionType } from "../types";
const filterModeOptions = {
[GQL.FilterMode.Galleries]: GalleryListFilterOptions.criterionOptions,
[GQL.FilterMode.Images]: ImageListFilterOptions.criterionOptions,
[GQL.FilterMode.Movies]: MovieListFilterOptions.criterionOptions,
[GQL.FilterMode.Performers]: PerformerListFilterOptions.criterionOptions,
[GQL.FilterMode.SceneMarkers]: SceneMarkerListFilterOptions.criterionOptions,
[GQL.FilterMode.Scenes]: SceneListFilterOptions.criterionOptions,
[GQL.FilterMode.Studios]: StudioListFilterOptions.criterionOptions,
[GQL.FilterMode.Tags]: TagListFilterOptions.criterionOptions,
};
export function makeCriteria(
config: GQL.ConfigDataFragment | undefined,
mode: GQL.FilterMode,
type: CriterionType = "none"
) {
switch (type) {
case "none":
return new NoneCriterion();
case "name":
return new StringCriterion(
new MandatoryStringCriterionOption(type, type)
);
case "path":
return new StringCriterion(new PathCriterionOption(type, type));
case "checksum":
return new StringCriterion(
new MandatoryStringCriterionOption("media_info.checksum", type, type)
);
case "oshash":
return new StringCriterion(
new MandatoryStringCriterionOption("media_info.hash", type, type)
);
case "organized":
return new OrganizedCriterion();
case "o_counter":
case "interactive_speed":
case "scene_count":
case "marker_count":
case "image_count":
case "gallery_count":
case "performer_count":
case "performer_age":
case "tag_count":
case "file_count":
case "play_count":
return new NumberCriterion(
new MandatoryNumberCriterionOption(type, type)
);
case "rating":
return new NumberCriterion(new NullNumberCriterionOption(type, type));
case "rating100":
return new RatingCriterion(
new NullNumberCriterionOption("rating", type),
(config?.ui as IUIConfig)?.ratingSystemOptions ??
defaultRatingSystemOptions
);
case "resolution":
return new ResolutionCriterion();
case "average_resolution":
return new AverageResolutionCriterion();
case "video_codec":
return new StringCriterion(new StringCriterionOption(type, type));
case "audio_codec":
return new StringCriterion(new StringCriterionOption(type, type));
case "resume_time":
case "duration":
case "play_duration":
return new DurationCriterion(
new MandatoryNumberCriterionOption(type, type)
);
case "favorite":
return new FavoriteCriterion();
case "hasMarkers":
return new HasMarkersCriterion();
case "hasChapters":
return new HasChaptersCriterion();
case "sceneIsMissing":
return new IsMissingCriterion(SceneIsMissingCriterionOption);
case "imageIsMissing":
return new IsMissingCriterion(ImageIsMissingCriterionOption);
case "performerIsMissing":
return new IsMissingCriterion(PerformerIsMissingCriterionOption);
case "galleryIsMissing":
return new IsMissingCriterion(GalleryIsMissingCriterionOption);
case "tagIsMissing":
return new IsMissingCriterion(TagIsMissingCriterionOption);
case "studioIsMissing":
return new IsMissingCriterion(StudioIsMissingCriterionOption);
case "movieIsMissing":
return new IsMissingCriterion(MovieIsMissingCriterionOption);
case "tags":
return new TagsCriterion(TagsCriterionOption);
case "sceneTags":
return new TagsCriterion(SceneTagsCriterionOption);
case "performerTags":
return new TagsCriterion(PerformerTagsCriterionOption);
case "parentTags":
return new TagsCriterion(ParentTagsCriterionOption);
case "childTags":
return new TagsCriterion(ChildTagsCriterionOption);
case "performers":
return new PerformersCriterion();
case "performer_favorite":
return new PerformerFavoriteCriterion();
case "studios":
return new StudiosCriterion();
case "parent_studios":
return new ParentStudiosCriterion();
case "movies":
return new ILabeledIdCriterion(MoviesCriterionOption);
case "galleries":
return new GalleriesCriterion();
case "birth_year":
case "death_year":
case "weight":
return new NumberCriterion(new NumberCriterionOption(type, type));
case "penis_length":
return new NumberCriterion(new NumberCriterionOption(type, type));
case "age":
return new NumberCriterion(
new MandatoryNumberCriterionOption(type, type)
);
case "gender":
return new GenderCriterion();
case "circumcised":
return new CircumcisedCriterion();
case "sceneChecksum":
case "galleryChecksum":
return new StringCriterion(
new StringCriterionOption("media_info.checksum", type, "checksum")
);
case "phash":
return new PhashCriterion();
case "duplicated":
return new DuplicatedCriterion();
case "country":
return new CountryCriterion();
case "height":
case "height_cm":
return new NumberCriterion(
new NumberCriterionOption("height", "height_cm", type)
);
// stash_id is deprecated
case "stash_id":
case "stash_id_endpoint":
return new StashIDCriterion();
case "ethnicity":
case "hair_color":
case "eye_color":
case "measurements":
case "fake_tits":
case "career_length":
case "tattoos":
case "piercings":
case "aliases":
case "url":
case "details":
case "title":
case "director":
case "synopsis":
case "description":
case "disambiguation":
return new StringCriterion(new StringCriterionOption(type, type));
case "scene_code":
return new StringCriterion(new StringCriterionOption(type, type, "code"));
case "interactive":
return new InteractiveCriterion();
case "captions":
return new CaptionCriterion();
case "parent_tag_count":
return new NumberCriterion(
new MandatoryNumberCriterionOption(
"parent_tag_count",
"parent_tag_count",
"parent_count"
)
);
case "child_tag_count":
return new NumberCriterion(
new MandatoryNumberCriterionOption(
"sub_tag_count",
"child_tag_count",
"child_count"
)
);
case "ignore_auto_tag":
return new BooleanCriterion(new BooleanCriterionOption(type, type));
case "date":
case "birthdate":
case "death_date":
case "scene_date":
return new DateCriterion(new DateCriterionOption(type, type));
case "created_at":
case "updated_at":
case "scene_created_at":
case "scene_updated_at":
return new TimestampCriterion(
new MandatoryTimestampCriterionOption(type, type)
);
const criterionOptions = filterModeOptions[mode];
const option = criterionOptions.find((o) => o.type === type);
if (!option) {
throw new Error(`Unknown criterion parameter name: ${type}`);
}
return option?.makeCriterion();
}

View file

@ -2,7 +2,6 @@ import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
export const FavoriteCriterionOption = new BooleanCriterionOption(
"favourite",
"favorite",
"filter_favorites"
);
@ -13,7 +12,6 @@ export class FavoriteCriterion extends BooleanCriterion {
}
export const PerformerFavoriteCriterionOption = new BooleanCriterionOption(
"performer_favorite",
"performer_favorite",
"performer_favorite"
);

View file

@ -1,10 +1,12 @@
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
const inputType = "galleries";
const galleriesCriterionOption = new ILabeledIdCriterionOption(
"galleries",
"galleries",
"galleries",
true
true,
inputType
);
export class GalleriesCriterion extends ILabeledIdCriterion {

View file

@ -6,6 +6,7 @@ export const GenderCriterionOption = new CriterionOption({
messageID: "gender",
type: "gender",
options: genderStrings,
makeCriterion: () => new GenderCriterion(),
});
export class GenderCriterion extends StringCriterion {

View file

@ -2,9 +2,9 @@ import { CriterionOption, StringCriterion } from "./criterion";
export const HasChaptersCriterionOption = new CriterionOption({
messageID: "hasChapters",
type: "hasChapters",
parameterName: "has_chapters",
type: "has_chapters",
options: [true.toString(), false.toString()],
makeCriterion: () => new HasChaptersCriterion(),
});
export class HasChaptersCriterion extends StringCriterion {

View file

@ -2,9 +2,9 @@ import { CriterionOption, StringCriterion } from "./criterion";
export const HasMarkersCriterionOption = new CriterionOption({
messageID: "hasMarkers",
type: "hasMarkers",
parameterName: "has_markers",
type: "has_markers",
options: [true.toString(), false.toString()],
makeCriterion: () => new HasMarkersCriterion(),
});
export class HasMarkersCriterion extends StringCriterion {

View file

@ -11,25 +11,19 @@ export class IsMissingCriterion extends StringCriterion {
}
class IsMissingCriterionOptionClass extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
parameterName: string,
options: Option[]
) {
constructor(messageID: string, type: CriterionType, options: Option[]) {
super({
messageID,
type: value,
parameterName,
type,
options,
defaultModifier: CriterionModifier.Equals,
makeCriterion: () => new IsMissingCriterion(this),
});
}
}
export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing",
"sceneIsMissing",
"is_missing",
[
"title",
@ -48,73 +42,59 @@ export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass(
export const ImageIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing",
"imageIsMissing",
"is_missing",
["title", "galleries", "studio", "performers", "tags"]
);
export const PerformerIsMissingCriterionOption =
new IsMissingCriterionOptionClass(
"isMissing",
"performerIsMissing",
"is_missing",
[
"url",
"twitter",
"instagram",
"ethnicity",
"country",
"hair_color",
"eye_color",
"height",
"weight",
"measurements",
"fake_tits",
"career_length",
"tattoos",
"piercings",
"aliases",
"gender",
"image",
"details",
"stash_id",
]
);
new IsMissingCriterionOptionClass("isMissing", "is_missing", [
"url",
"twitter",
"instagram",
"ethnicity",
"country",
"hair_color",
"eye_color",
"height",
"weight",
"measurements",
"fake_tits",
"career_length",
"tattoos",
"piercings",
"aliases",
"gender",
"image",
"details",
"stash_id",
]);
export const GalleryIsMissingCriterionOption =
new IsMissingCriterionOptionClass(
"isMissing",
"galleryIsMissing",
"is_missing",
[
"title",
"details",
"url",
"date",
"studio",
"performers",
"tags",
"scenes",
]
);
new IsMissingCriterionOptionClass("isMissing", "is_missing", [
"title",
"details",
"url",
"date",
"studio",
"performers",
"tags",
"scenes",
]);
export const TagIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing",
"tagIsMissing",
"is_missing",
["image"]
);
export const StudioIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing",
"studioIsMissing",
"is_missing",
["image", "stash_id", "details"]
);
export const MovieIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing",
"movieIsMissing",
"is_missing",
["front_image", "back_image", "scenes"]
);

View file

@ -1,10 +1,12 @@
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
const inputType = "movies";
export const MoviesCriterionOption = new ILabeledIdCriterionOption(
"movies",
"movies",
"movies",
false
false,
inputType
);
export class MoviesCriterion extends ILabeledIdCriterion {

View file

@ -1,10 +1,6 @@
import { Criterion, StringCriterionOption } from "./criterion";
export const NoneCriterionOption = new StringCriterionOption(
"none",
"none",
"none"
);
export const NoneCriterionOption = new StringCriterionOption("none", "none");
export class NoneCriterion extends Criterion<string> {
constructor() {
super(NoneCriterionOption, "none");

View file

@ -1,7 +1,6 @@
import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
export const OrganizedCriterionOption = new BooleanCriterionOption(
"organized",
"organized",
"organized"
);

View file

@ -17,12 +17,15 @@ const modifierOptions = [
const defaultModifier = CriterionModifier.IncludesAll;
const inputType = "performers";
export const PerformersCriterionOption = new CriterionOption({
messageID: "performers",
type: "performers",
parameterName: "performers",
modifierOptions,
defaultModifier,
makeCriterion: () => new PerformersCriterion(),
inputType,
});
export class PerformersCriterion extends Criterion<ILabeledValueListValue> {

View file

@ -12,8 +12,7 @@ import {
export const PhashCriterionOption = new CriterionOption({
messageID: "media_info.phash",
type: "phash",
parameterName: "phash_distance",
type: "phash_distance",
inputType: "text",
modifierOptions: [
CriterionModifier.Equals,
@ -21,6 +20,7 @@ export const PhashCriterionOption = new CriterionOption({
CriterionModifier.IsNull,
CriterionModifier.NotNull,
],
makeCriterion: () => new PhashCriterion(),
});
export class PhashCriterion extends Criterion<IPhashDistanceValue> {
@ -53,7 +53,7 @@ export class PhashCriterion extends Criterion<IPhashDistanceValue> {
export const DuplicatedCriterionOption = new BooleanCriterionOption(
"duplicated_phash",
"duplicated",
"duplicated"
() => new DuplicatedCriterion()
);
export class DuplicatedCriterion extends PhashDuplicateCriterion {

View file

@ -4,7 +4,12 @@ import {
} from "src/core/generated-graphql";
import { stringToResolution, resolutionStrings } from "src/utils/resolution";
import { CriterionType } from "../types";
import { CriterionOption, StringCriterion } from "./criterion";
import {
Criterion,
CriterionOption,
CriterionValue,
StringCriterion,
} from "./criterion";
abstract class AbstractResolutionCriterion extends StringCriterion {
protected toCriterionInput(): ResolutionCriterionInput | undefined {
@ -20,11 +25,13 @@ abstract class AbstractResolutionCriterion extends StringCriterion {
}
class ResolutionCriterionOptionType extends CriterionOption {
constructor(value: CriterionType) {
constructor(
value: CriterionType,
makeCriterion: () => Criterion<CriterionValue>
) {
super({
messageID: value,
type: value,
parameterName: value,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
@ -32,12 +39,14 @@ class ResolutionCriterionOptionType extends CriterionOption {
CriterionModifier.LessThan,
],
options: resolutionStrings,
makeCriterion,
});
}
}
export const ResolutionCriterionOption = new ResolutionCriterionOptionType(
"resolution"
"resolution",
() => new ResolutionCriterion()
);
export class ResolutionCriterion extends AbstractResolutionCriterion {
constructor() {
@ -46,7 +55,10 @@ export class ResolutionCriterion extends AbstractResolutionCriterion {
}
export const AverageResolutionCriterionOption =
new ResolutionCriterionOptionType("average_resolution");
new ResolutionCriterionOptionType(
"average_resolution",
() => new AverageResolutionCriterion()
);
export class AverageResolutionCriterion extends AbstractResolutionCriterion {
constructor() {

View file

@ -10,13 +10,13 @@ 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,
],
makeCriterion: () => new StashIDCriterion(),
});
export class StashIDCriterion extends Criterion<IStashIDValue> {

View file

@ -13,13 +13,15 @@ const modifierOptions = [
];
const defaultModifier = CriterionModifier.Includes;
const inputType = "studios";
export const StudiosCriterionOption = new CriterionOption({
messageID: "studios",
type: "studios",
parameterName: "studios",
modifierOptions,
defaultModifier,
makeCriterion: () => new StudiosCriterion(),
inputType,
});
export class StudiosCriterion extends IHierarchicalLabeledIdCriterion {
@ -29,10 +31,10 @@ export class StudiosCriterion extends IHierarchicalLabeledIdCriterion {
}
export const ParentStudiosCriterionOption = new ILabeledIdCriterionOption(
"parent_studios",
"parent_studios",
"parents",
false
false,
inputType
);
export class ParentStudiosCriterion extends ILabeledIdCriterion {
constructor() {

View file

@ -1,7 +1,8 @@
import { CriterionModifier } from "src/core/generated-graphql";
import { CriterionOption, IHierarchicalLabeledIdCriterion } from "./criterion";
import { CriterionType } from "../types";
const modifierOptions = [
const defaultModifierOptions = [
CriterionModifier.IncludesAll,
CriterionModifier.Includes,
CriterionModifier.Equals,
@ -17,41 +18,53 @@ const withoutEqualsModifierOptions = [
];
const defaultModifier = CriterionModifier.IncludesAll;
const inputType = "tags";
export const TagsCriterionOption = new CriterionOption({
messageID: "tags",
type: "tags",
parameterName: "tags",
modifierOptions,
defaultModifier,
});
export const SceneTagsCriterionOption = new CriterionOption({
messageID: "sceneTags",
type: "sceneTags",
parameterName: "scene_tags",
modifierOptions,
defaultModifier,
});
export const PerformerTagsCriterionOption = new CriterionOption({
messageID: "performerTags",
type: "performerTags",
parameterName: "performer_tags",
modifierOptions: withoutEqualsModifierOptions,
defaultModifier,
});
export const ParentTagsCriterionOption = new CriterionOption({
messageID: "parent_tags",
type: "parentTags",
parameterName: "parents",
modifierOptions: withoutEqualsModifierOptions,
defaultModifier,
});
export const ChildTagsCriterionOption = new CriterionOption({
messageID: "sub_tags",
type: "childTags",
parameterName: "children",
modifierOptions: withoutEqualsModifierOptions,
defaultModifier,
});
export class TagsCriterionOptionClass extends CriterionOption {
constructor(
messageID: string,
type: CriterionType,
modifierOptions: CriterionModifier[]
) {
super({
messageID,
type,
modifierOptions,
defaultModifier,
makeCriterion: () => new TagsCriterion(this),
inputType,
});
}
}
export const TagsCriterionOption = new TagsCriterionOptionClass(
"tags",
"tags",
defaultModifierOptions
);
export const SceneTagsCriterionOption = new TagsCriterionOptionClass(
"scene_tags",
"scene_tags",
defaultModifierOptions
);
export const PerformerTagsCriterionOption = new TagsCriterionOptionClass(
"performer_tags",
"performer_tags",
withoutEqualsModifierOptions
);
export const ParentTagsCriterionOption = new TagsCriterionOptionClass(
"parent_tags",
"parents",
withoutEqualsModifierOptions
);
export const ChildTagsCriterionOption = new TagsCriterionOptionClass(
"sub_tags",
"children",
withoutEqualsModifierOptions
);
export class TagsCriterion extends IHierarchicalLabeledIdCriterion {}

View file

@ -2,11 +2,12 @@ import {
ConfigDataFragment,
FilterMode,
FindFilterType,
SavedFilterDataFragment,
SortDirectionEnum,
} from "src/core/generated-graphql";
import { Criterion, CriterionValue } from "./criteria/criterion";
import { makeCriteria } from "./criteria/factory";
import { DisplayMode } from "./types";
import { CriterionType, DisplayMode } from "./types";
interface IDecodedParams {
perPage?: number;
@ -127,7 +128,7 @@ export class ListFilterModel {
for (const jsonString of params.c) {
try {
const encodedCriterion = JSON.parse(jsonString);
const criterion = makeCriteria(this.config, encodedCriterion.type);
const criterion = makeCriteria(this.mode, encodedCriterion.type);
// it's possible that we have unsupported criteria. Just skip if so.
if (criterion) {
criterion.setFromEncodedCriterion(encodedCriterion);
@ -248,8 +249,41 @@ export class ListFilterModel {
this.configureFromDecodedParams(decoded);
}
public configureFromJSON(json: string) {
this.configureFromDecodedParams(JSON.parse(json));
public configureFromSavedFilter(savedFilter: SavedFilterDataFragment) {
const {
find_filter: findFilter,
object_filter: objectFilter,
ui_options: uiOptions,
} = savedFilter;
this.itemsPerPage = findFilter?.per_page ?? this.itemsPerPage;
this.sortBy = findFilter?.sort ?? this.sortBy;
// parse the random seed if provided
const match = this.sortBy?.match(/^random_(\d+)$/);
if (match) {
this.sortBy = "random";
this.randomSeed = Number.parseInt(match[1], 10);
}
this.sortDirection =
(findFilter?.direction as SortDirectionEnum) ?? this.sortDirection;
this.searchTerm = findFilter?.q ?? this.searchTerm;
this.displayMode = uiOptions?.display_mode ?? this.displayMode;
this.zoomIndex = uiOptions?.zoom_index ?? this.zoomIndex;
this.currentPage = 1;
this.criteria = [];
if (objectFilter) {
Object.keys(objectFilter).forEach((key) => {
const criterion = makeCriteria(this.mode, key as CriterionType);
// it's possible that we have unsupported criteria. Just skip if so.
if (criterion) {
criterion.setFromEncodedCriterion(objectFilter[key]);
this.criteria.push(criterion);
}
});
}
}
private setRandomSeed() {
@ -405,4 +439,22 @@ export class ListFilterModel {
return output;
}
public makeSavedFindFilter() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const output: Record<string, any> = {};
this.criteria.forEach((criterion) => {
criterion.toSavedFilter(output);
});
return output;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public makeUIOptions(): Record<string, any> {
return {
display_mode: this.displayMode,
zoom_index: this.zoomIndex,
};
}
}

View file

@ -45,11 +45,7 @@ const criterionOptions = [
createStringCriterionOption("title"),
createStringCriterionOption("details"),
createPathCriterionOption("path"),
createStringCriterionOption(
"galleryChecksum",
"media_info.checksum",
"checksum"
),
createStringCriterionOption("checksum", "media_info.checksum"),
new NullNumberCriterionOption("rating", "rating100"),
OrganizedCriterionOption,
AverageResolutionCriterionOption,

View file

@ -94,7 +94,7 @@ const criterionOptions = [
createMandatoryNumberCriterionOption("gallery_count"),
createMandatoryNumberCriterionOption("o_counter"),
createBooleanCriterionOption("ignore_auto_tag"),
new NumberCriterionOption("height", "height_cm", "height_cm"),
new NumberCriterionOption("height", "height_cm"),
...numberCriteria.map((c) => createNumberCriterionOption(c)),
...stringCriteria.map((c) => createStringCriterionOption(c)),
createDateCriterionOption("birthdate"),

View file

@ -59,16 +59,12 @@ const displayModeOptions = [
const criterionOptions = [
createStringCriterionOption("title"),
createStringCriterionOption("scene_code"),
createStringCriterionOption("code", "scene_code"),
createPathCriterionOption("path"),
createStringCriterionOption("details"),
createStringCriterionOption("director"),
createMandatoryStringCriterionOption("oshash", "media_info.hash"),
createStringCriterionOption(
"sceneChecksum",
"media_info.checksum",
"checksum"
),
createStringCriterionOption("checksum", "media_info.checksum"),
PhashCriterionOption,
DuplicatedCriterionOption,
OrganizedCriterionOption,

View file

@ -53,17 +53,9 @@ const criterionOptions = [
createMandatoryNumberCriterionOption("performer_count"),
createMandatoryNumberCriterionOption("marker_count"),
ParentTagsCriterionOption,
new MandatoryNumberCriterionOption(
"parent_tag_count",
"parent_tag_count",
"parent_count"
),
new MandatoryNumberCriterionOption("parent_tag_count", "parent_count"),
ChildTagsCriterionOption,
new MandatoryNumberCriterionOption(
"sub_tag_count",
"child_tag_count",
"child_count"
),
new MandatoryNumberCriterionOption("sub_tag_count", "child_count"),
createMandatoryTimestampCriterionOption("created_at"),
createMandatoryTimestampCriterionOption("updated_at"),
];

View file

@ -112,20 +112,12 @@ export type CriterionType =
| "video_codec"
| "audio_codec"
| "duration"
| "favorite"
| "hasMarkers"
| "sceneIsMissing"
| "imageIsMissing"
| "performerIsMissing"
| "galleryIsMissing"
| "tagIsMissing"
| "studioIsMissing"
| "movieIsMissing"
| "filter_favorites"
| "has_markers"
| "is_missing"
| "tags"
| "sceneTags"
| "performerTags"
| "parentTags"
| "childTags"
| "scene_tags"
| "performer_tags"
| "tag_count"
| "performers"
| "studios"
@ -149,7 +141,8 @@ export type CriterionType =
| "piercings"
| "aliases"
| "gender"
| "parent_studios"
| "parents"
| "children"
| "scene_count"
| "marker_count"
| "image_count"
@ -169,13 +162,11 @@ export type CriterionType =
| "title"
| "oshash"
| "checksum"
| "sceneChecksum"
| "galleryChecksum"
| "phash"
| "phash_distance"
| "director"
| "synopsis"
| "parent_tag_count"
| "child_tag_count"
| "parent_count"
| "child_count"
| "performer_favorite"
| "performer_age"
| "duplicated"
@ -191,6 +182,6 @@ export type CriterionType =
| "scene_created_at"
| "scene_updated_at"
| "description"
| "scene_code"
| "code"
| "disambiguation"
| "hasChapters";
| "has_chapters";