Add O-Counter for Performers and Sort/Filter Performers by O-Counter (#3588)

* initial commit of sort performer by o-count

* work on o_counter filter

* filter working

* sorting, filtering using combined scene+image count

* linting

* fix performer list view

---------

Co-authored-by: jpnsfw <none@none.com>
This commit is contained in:
jpnsfw
2023-04-24 17:01:41 -04:00
committed by GitHub
parent 152f9114b2
commit 64b7934af2
18 changed files with 210 additions and 1 deletions

View File

@@ -25,6 +25,7 @@ fragment PerformerData on Performer {
image_count image_count
gallery_count gallery_count
movie_count movie_count
o_counter
tags { tags {
...SlimTagData ...SlimTagData

View File

@@ -98,6 +98,8 @@ input PerformerFilterType {
image_count: IntCriterionInput image_count: IntCriterionInput
"""Filter by gallery count""" """Filter by gallery count"""
gallery_count: IntCriterionInput gallery_count: IntCriterionInput
"""Filter by o count"""
o_counter: IntCriterionInput
"""Filter by StashID""" """Filter by StashID"""
stash_id: StringCriterionInput @deprecated(reason: "Use stash_id_endpoint instead") stash_id: StringCriterionInput @deprecated(reason: "Use stash_id_endpoint instead")
"""Filter by StashID""" """Filter by StashID"""

View File

@@ -37,6 +37,7 @@ type Performer {
scene_count: Int # Resolver scene_count: Int # Resolver
image_count: Int # Resolver image_count: Int # Resolver
gallery_count: Int # Resolver gallery_count: Int # Resolver
o_counter: Int # Resolver
scenes: [Scene!]! scenes: [Scene!]!
stash_ids: [StashID!]! stash_ids: [StashID!]!
# rating expressed as 1-5 # rating expressed as 1-5

View File

@@ -127,6 +127,24 @@ func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Perfor
return &res, nil return &res, nil
} }
func (r *performerResolver) OCounter(ctx context.Context, obj *models.Performer) (ret *int, err error) {
var res_scene int
var res_image int
var res int
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res_scene, err = r.repository.Scene.OCountByPerformerID(ctx, obj.ID)
if err != nil {
return err
}
res_image, err = r.repository.Image.OCountByPerformerID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
}
res = res_scene + res_image
return &res, nil
}
func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (ret []*models.Scene, err error) { func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (ret []*models.Scene, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error { if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Scene.FindByPerformerID(ctx, obj.ID) ret, err = r.repository.Scene.FindByPerformerID(ctx, obj.ID)

View File

@@ -108,6 +108,7 @@ type ImageReader interface {
FindByChecksum(ctx context.Context, checksum string) ([]*Image, error) FindByChecksum(ctx context.Context, checksum string) ([]*Image, error)
FindByGalleryID(ctx context.Context, galleryID int) ([]*Image, error) FindByGalleryID(ctx context.Context, galleryID int) ([]*Image, error)
CountByGalleryID(ctx context.Context, galleryID int) (int, error) CountByGalleryID(ctx context.Context, galleryID int) (int, error)
OCountByPerformerID(ctx context.Context, performerID int) (int, error)
Count(ctx context.Context) (int, error) Count(ctx context.Context) (int, error)
Size(ctx context.Context) (float64, error) Size(ctx context.Context) (float64, error)
All(ctx context.Context) ([]*Image, error) All(ctx context.Context) ([]*Image, error)

View File

@@ -79,6 +79,27 @@ func (_m *ImageReaderWriter) CountByGalleryID(ctx context.Context, galleryID int
return r0, r1 return r0, r1
} }
// OCountByPerformerID provides a mock function with given fields: ctx, performerID
func (_m *ImageReaderWriter) OCountByPerformerID(ctx context.Context, performerID int) (int, error) {
ret := _m.Called(ctx, performerID)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
r0 = rf(ctx, performerID)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, performerID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Create provides a mock function with given fields: ctx, newImage // Create provides a mock function with given fields: ctx, newImage
func (_m *ImageReaderWriter) Create(ctx context.Context, newImage *models.ImageCreateInput) error { func (_m *ImageReaderWriter) Create(ctx context.Context, newImage *models.ImageCreateInput) error {
ret := _m.Called(ctx, newImage) ret := _m.Called(ctx, newImage)

View File

@@ -102,6 +102,27 @@ func (_m *SceneReaderWriter) CountByPerformerID(ctx context.Context, performerID
return r0, r1 return r0, r1
} }
// OCountByPerformerID provides a mock function with given fields: ctx, performerID
func (_m *SceneReaderWriter) OCountByPerformerID(ctx context.Context, performerID int) (int, error) {
ret := _m.Called(ctx, performerID)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
r0 = rf(ctx, performerID)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, performerID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountByStudioID provides a mock function with given fields: ctx, studioID // CountByStudioID provides a mock function with given fields: ctx, studioID
func (_m *SceneReaderWriter) CountByStudioID(ctx context.Context, studioID int) (int, error) { func (_m *SceneReaderWriter) CountByStudioID(ctx context.Context, studioID int) (int, error) {
ret := _m.Called(ctx, studioID) ret := _m.Called(ctx, studioID)

View File

@@ -110,6 +110,8 @@ type PerformerFilterType struct {
ImageCount *IntCriterionInput `json:"image_count"` ImageCount *IntCriterionInput `json:"image_count"`
// Filter by gallery count // Filter by gallery count
GalleryCount *IntCriterionInput `json:"gallery_count"` GalleryCount *IntCriterionInput `json:"gallery_count"`
// Filter by O count
OCounter *IntCriterionInput `json:"o_counter"`
// Filter by StashID // Filter by StashID
StashID *StringCriterionInput `json:"stash_id"` StashID *StringCriterionInput `json:"stash_id"`
// Filter by StashID Endpoint // Filter by StashID Endpoint

View File

@@ -163,6 +163,7 @@ type SceneReader interface {
VideoFileLoader VideoFileLoader
CountByPerformerID(ctx context.Context, performerID int) (int, error) CountByPerformerID(ctx context.Context, performerID int) (int, error)
OCountByPerformerID(ctx context.Context, performerID int) (int, error)
// FindByStudioID(studioID int) ([]*Scene, error) // FindByStudioID(studioID int) ([]*Scene, error)
FindByMovieID(ctx context.Context, movieID int) ([]*Scene, error) FindByMovieID(ctx context.Context, movieID int) ([]*Scene, error)
CountByMovieID(ctx context.Context, movieID int) (int, error) CountByMovieID(ctx context.Context, movieID int) (int, error)

View File

@@ -332,6 +332,7 @@ type Performer struct {
Deleted bool `json:"deleted"` Deleted bool `json:"deleted"`
Edits []*Edit `json:"edits,omitempty"` Edits []*Edit `json:"edits,omitempty"`
SceneCount int `json:"scene_count"` SceneCount int `json:"scene_count"`
OCounter int `json:"o_counter"`
MergedIds []string `json:"merged_ids,omitempty"` MergedIds []string `json:"merged_ids,omitempty"`
Studios []*PerformerStudio `json:"studios,omitempty"` Studios []*PerformerStudio `json:"studios,omitempty"`
IsFavorite bool `json:"is_favorite"` IsFavorite bool `json:"is_favorite"`
@@ -1771,6 +1772,7 @@ const (
PerformerSortEnumName PerformerSortEnum = "NAME" PerformerSortEnumName PerformerSortEnum = "NAME"
PerformerSortEnumBirthdate PerformerSortEnum = "BIRTHDATE" PerformerSortEnumBirthdate PerformerSortEnum = "BIRTHDATE"
PerformerSortEnumSceneCount PerformerSortEnum = "SCENE_COUNT" PerformerSortEnumSceneCount PerformerSortEnum = "SCENE_COUNT"
PerformerSortEnumOCounter PerformerSortEnum = "O_COUNTER"
PerformerSortEnumCareerStartYear PerformerSortEnum = "CAREER_START_YEAR" PerformerSortEnumCareerStartYear PerformerSortEnum = "CAREER_START_YEAR"
PerformerSortEnumDebut PerformerSortEnum = "DEBUT" PerformerSortEnumDebut PerformerSortEnum = "DEBUT"
PerformerSortEnumCreatedAt PerformerSortEnum = "CREATED_AT" PerformerSortEnumCreatedAt PerformerSortEnum = "CREATED_AT"
@@ -1781,6 +1783,7 @@ var AllPerformerSortEnum = []PerformerSortEnum{
PerformerSortEnumName, PerformerSortEnumName,
PerformerSortEnumBirthdate, PerformerSortEnumBirthdate,
PerformerSortEnumSceneCount, PerformerSortEnumSceneCount,
PerformerSortEnumOCounter,
PerformerSortEnumCareerStartYear, PerformerSortEnumCareerStartYear,
PerformerSortEnumDebut, PerformerSortEnumDebut,
PerformerSortEnumCreatedAt, PerformerSortEnumCreatedAt,
@@ -1789,7 +1792,7 @@ var AllPerformerSortEnum = []PerformerSortEnum{
func (e PerformerSortEnum) IsValid() bool { func (e PerformerSortEnum) IsValid() bool {
switch e { switch e {
case PerformerSortEnumName, PerformerSortEnumBirthdate, PerformerSortEnumSceneCount, PerformerSortEnumCareerStartYear, PerformerSortEnumDebut, PerformerSortEnumCreatedAt, PerformerSortEnumUpdatedAt: case PerformerSortEnumName, PerformerSortEnumBirthdate, PerformerSortEnumSceneCount, PerformerSortEnumOCounter, PerformerSortEnumCareerStartYear, PerformerSortEnumDebut, PerformerSortEnumCreatedAt, PerformerSortEnumUpdatedAt:
return true return true
} }
return false return false

View File

@@ -722,6 +722,28 @@ func (m *countCriterionHandlerBuilder) handler(criterion *models.IntCriterionInp
} }
} }
type joinedMultiSumCriterionHandlerBuilder struct {
primaryTable string
foreignTable1 string
joinTable1 string
foreignTable2 string
joinTable2 string
primaryFK string
foreignFK1 string
foreignFK2 string
sum string
}
func (m *joinedMultiSumCriterionHandlerBuilder) handler(criterion *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if criterion != nil {
clause, args := getJoinedMultiSumCriterionClause(m.primaryTable, m.foreignTable1, m.joinTable1, m.foreignTable2, m.joinTable2, m.primaryFK, m.foreignFK1, m.foreignFK2, m.sum, *criterion)
f.addWhere(clause, args...)
}
}
}
// handler for StringCriterion for string list fields // handler for StringCriterion for string list fields
type stringListCriterionHandlerBuilder struct { type stringListCriterionHandlerBuilder struct {
// table joining primary and foreign objects // table joining primary and foreign objects

View File

@@ -513,6 +513,19 @@ func (qb *ImageStore) CountByGalleryID(ctx context.Context, galleryID int) (int,
return count(ctx, q) return count(ctx, q)
} }
func (qb *ImageStore) OCountByPerformerID(ctx context.Context, performerID int) (int, error) {
table := qb.table()
joinTable := performersImagesJoinTable
q := dialect.Select(goqu.COALESCE(goqu.SUM("o_counter"), 0)).From(table).InnerJoin(joinTable, goqu.On(table.Col(idColumn).Eq(joinTable.Col(imageIDColumn)))).Where(joinTable.Col(performerIDColumn).Eq(performerID))
var ret int
if err := querySimple(ctx, q, &ret); err != nil {
return 0, err
}
return ret, nil
}
func (qb *ImageStore) FindByFolderID(ctx context.Context, folderID file.FolderID) ([]*models.Image, error) { func (qb *ImageStore) FindByFolderID(ctx context.Context, folderID file.FolderID) ([]*models.Image, error) {
table := qb.table() table := qb.table()
fileTable := goqu.T(fileTable) fileTable := goqu.T(fileTable)

View File

@@ -629,6 +629,7 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform
query.handleCriterion(ctx, performerSceneCountCriterionHandler(qb, filter.SceneCount)) query.handleCriterion(ctx, performerSceneCountCriterionHandler(qb, filter.SceneCount))
query.handleCriterion(ctx, performerImageCountCriterionHandler(qb, filter.ImageCount)) query.handleCriterion(ctx, performerImageCountCriterionHandler(qb, filter.ImageCount))
query.handleCriterion(ctx, performerGalleryCountCriterionHandler(qb, filter.GalleryCount)) query.handleCriterion(ctx, performerGalleryCountCriterionHandler(qb, filter.GalleryCount))
query.handleCriterion(ctx, performerOCounterCriterionHandler(qb, filter.OCounter))
query.handleCriterion(ctx, dateCriterionHandler(filter.Birthdate, tableName+".birthdate")) query.handleCriterion(ctx, dateCriterionHandler(filter.Birthdate, tableName+".birthdate"))
query.handleCriterion(ctx, dateCriterionHandler(filter.DeathDate, tableName+".death_date")) query.handleCriterion(ctx, dateCriterionHandler(filter.DeathDate, tableName+".death_date"))
query.handleCriterion(ctx, timestampCriterionHandler(filter.CreatedAt, tableName+".created_at")) query.handleCriterion(ctx, timestampCriterionHandler(filter.CreatedAt, tableName+".created_at"))
@@ -805,6 +806,22 @@ func performerGalleryCountCriterionHandler(qb *PerformerStore, count *models.Int
return h.handler(count) return h.handler(count)
} }
func performerOCounterCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
h := joinedMultiSumCriterionHandlerBuilder{
primaryTable: performerTable,
foreignTable1: sceneTable,
joinTable1: performersScenesTable,
foreignTable2: imageTable,
joinTable2: performersImagesTable,
primaryFK: performerIDColumn,
foreignFK1: sceneIDColumn,
foreignFK2: imageIDColumn,
sum: "o_counter",
}
return h.handler(count)
}
func performerStudiosCriterionHandler(qb *PerformerStore, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func performerStudiosCriterionHandler(qb *PerformerStore, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) { return func(ctx context.Context, f *filterBuilder) {
if studios != nil { if studios != nil {
@@ -906,6 +923,9 @@ func (qb *PerformerStore) getPerformerSort(findFilter *models.FindFilterType) st
default: default:
sortQuery += getSort(sort, direction, "performers") sortQuery += getSort(sort, direction, "performers")
} }
if sort == "o_counter" {
return getMultiSumSort("o_counter", performerTable, sceneTable, performersScenesTable, imageTable, performersImagesTable, performerIDColumn, sceneIDColumn, imageIDColumn, direction)
}
// Whatever the sorting, always use name/id as a final sort // Whatever the sorting, always use name/id as a final sort
sortQuery += ", COALESCE(performers.name, performers.id) COLLATE NATURAL_CI ASC" sortQuery += ", COALESCE(performers.name, performers.id) COLLATE NATURAL_CI ASC"

View File

@@ -680,6 +680,19 @@ func (qb *SceneStore) CountByPerformerID(ctx context.Context, performerID int) (
return count(ctx, q) return count(ctx, q)
} }
func (qb *SceneStore) OCountByPerformerID(ctx context.Context, performerID int) (int, error) {
table := qb.table()
joinTable := scenesPerformersJoinTable
q := dialect.Select(goqu.COALESCE(goqu.SUM("o_counter"), 0)).From(table).InnerJoin(joinTable, goqu.On(table.Col(idColumn).Eq(joinTable.Col(sceneIDColumn)))).Where(joinTable.Col(performerIDColumn).Eq(performerID))
var ret int
if err := querySimple(ctx, q, &ret); err != nil {
return 0, err
}
return ret, nil
}
func (qb *SceneStore) FindByMovieID(ctx context.Context, movieID int) ([]*models.Scene, error) { func (qb *SceneStore) FindByMovieID(ctx context.Context, movieID int) ([]*models.Scene, error) {
sq := dialect.From(scenesMoviesJoinTable).Select(scenesMoviesJoinTable.Col(sceneIDColumn)).Where( sq := dialect.From(scenesMoviesJoinTable).Select(scenesMoviesJoinTable.Col(sceneIDColumn)).Where(
scenesMoviesJoinTable.Col(movieIDColumn).Eq(movieID), scenesMoviesJoinTable.Col(movieIDColumn).Eq(movieID),

View File

@@ -103,6 +103,27 @@ func getCountSort(primaryTable, joinTable, primaryFK, direction string) string {
return fmt.Sprintf(" ORDER BY (SELECT COUNT(*) FROM %s WHERE %s = %s.id) %s", joinTable, primaryFK, primaryTable, getSortDirection(direction)) return fmt.Sprintf(" ORDER BY (SELECT COUNT(*) FROM %s WHERE %s = %s.id) %s", joinTable, primaryFK, primaryTable, getSortDirection(direction))
} }
func getMultiSumSort(sum string, primaryTable, foreignTable1, joinTable1, foreignTable2, joinTable2, primaryFK, foreignFK1, foreignFK2, direction string) string {
return fmt.Sprintf(" ORDER BY (SELECT SUM(%s) "+
"FROM ("+
"SELECT SUM(%s) as %s from %s s "+
"LEFT JOIN %s ON %s.id = s.%s "+
"WHERE s.%s = %s.id "+
"UNION ALL "+
"SELECT SUM(%s) as %s from %s s "+
"LEFT JOIN %s ON %s.id = s.%s "+
"WHERE s.%s = %s.id "+
")) %s",
sum,
sum, sum, joinTable1,
foreignTable1, foreignTable1, foreignFK1,
primaryFK, primaryTable,
sum, sum, joinTable2,
foreignTable2, foreignTable2, foreignFK2,
primaryFK, primaryTable,
getSortDirection(direction))
}
func getStringSearchClause(columns []string, q string, not bool) sqlClause { func getStringSearchClause(columns []string, q string, not bool) sqlClause {
var likeClauses []string var likeClauses []string
var args []interface{} var args []interface{}
@@ -287,6 +308,28 @@ func getCountCriterionClause(primaryTable, joinTable, primaryFK string, criterio
return getIntCriterionWhereClause(lhs, criterion) return getIntCriterionWhereClause(lhs, criterion)
} }
func getJoinedMultiSumCriterionClause(primaryTable, foreignTable1, joinTable1, foreignTable2, joinTable2, primaryFK string, foreignFK1 string, foreignFK2 string, sum string, criterion models.IntCriterionInput) (string, []interface{}) {
lhs := fmt.Sprintf("(SELECT SUM(%s) "+
"FROM ("+
"SELECT SUM(%s) as %s from %s s "+
"LEFT JOIN %s ON %s.id = s.%s "+
"WHERE s.%s = %s.id "+
"UNION ALL "+
"SELECT SUM(%s) as %s from %s s "+
"LEFT JOIN %s ON %s.id = s.%s "+
"WHERE s.%s = %s.id "+
"))",
sum,
sum, sum, joinTable1,
foreignTable1, foreignTable1, foreignFK1,
primaryFK, primaryTable,
sum, sum, joinTable2,
foreignTable2, foreignTable2, foreignFK2,
primaryFK, primaryTable,
)
return getIntCriterionWhereClause(lhs, criterion)
}
func coalesce(column string) string { func coalesce(column string) string {
return fmt.Sprintf("COALESCE(%s, '')", column) return fmt.Sprintf("COALESCE(%s, '')", column)
} }

View File

@@ -6,6 +6,7 @@ import NavUtils from "src/utils/navigation";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { GridCard } from "../Shared/GridCard"; import { GridCard } from "../Shared/GridCard";
import { CountryFlag } from "../Shared/CountryFlag"; import { CountryFlag } from "../Shared/CountryFlag";
import { SweatDrops } from "../Shared/SweatDrops";
import { HoverPopover } from "../Shared/HoverPopover"; import { HoverPopover } from "../Shared/HoverPopover";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { TagLink } from "../Shared/TagLink"; import { TagLink } from "../Shared/TagLink";
@@ -137,6 +138,21 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
); );
} }
function maybeRenderOCounter() {
if (!performer.o_counter) return;
return (
<div className="o-counter">
<Button className="minimal">
<span className="fa-icon">
<SweatDrops />
</span>
<span>{performer.o_counter}</span>
</Button>
</div>
);
}
function maybeRenderTagPopoverButton() { function maybeRenderTagPopoverButton() {
if (performer.tags.length <= 0) return; if (performer.tags.length <= 0) return;
@@ -173,6 +189,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
performer.image_count || performer.image_count ||
performer.gallery_count || performer.gallery_count ||
performer.tags.length > 0 || performer.tags.length > 0 ||
performer.o_counter ||
performer.movie_count performer.movie_count
) { ) {
return ( return (
@@ -184,6 +201,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
{maybeRenderImagesPopoverButton()} {maybeRenderImagesPopoverButton()}
{maybeRenderGalleriesPopoverButton()} {maybeRenderGalleriesPopoverButton()}
{maybeRenderTagPopoverButton()} {maybeRenderTagPopoverButton()}
{maybeRenderOCounter()}
</ButtonGroup> </ButtonGroup>
</> </>
); );

View File

@@ -97,6 +97,9 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
<h6>{performer.gallery_count}</h6> <h6>{performer.gallery_count}</h6>
</Link> </Link>
</td> </td>
<td>
<h6>{performer.o_counter}</h6>
</td>
<td>{performer.birthdate}</td> <td>{performer.birthdate}</td>
<td>{!!performer.height_cm && formatHeight(performer.height_cm)}</td> <td>{!!performer.height_cm && formatHeight(performer.height_cm)}</td>
</tr> </tr>
@@ -114,6 +117,7 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
<th>{intl.formatMessage({ id: "scene_count" })}</th> <th>{intl.formatMessage({ id: "scene_count" })}</th>
<th>{intl.formatMessage({ id: "image_count" })}</th> <th>{intl.formatMessage({ id: "image_count" })}</th>
<th>{intl.formatMessage({ id: "gallery_count" })}</th> <th>{intl.formatMessage({ id: "gallery_count" })}</th>
<th>{intl.formatMessage({ id: "o_counter" })}</th>
<th>{intl.formatMessage({ id: "birthdate" })}</th> <th>{intl.formatMessage({ id: "birthdate" })}</th>
<th>{intl.formatMessage({ id: "height" })}</th> <th>{intl.formatMessage({ id: "height" })}</th>
</tr> </tr>

View File

@@ -40,6 +40,10 @@ const sortByOptions = [
messageID: "gallery_count", messageID: "gallery_count",
value: "galleries_count", value: "galleries_count",
}, },
{
messageID: "o_counter",
value: "o_counter",
},
]); ]);
const displayModeOptions = [ const displayModeOptions = [
@@ -84,6 +88,7 @@ const criterionOptions = [
createMandatoryNumberCriterionOption("scene_count"), createMandatoryNumberCriterionOption("scene_count"),
createMandatoryNumberCriterionOption("image_count"), createMandatoryNumberCriterionOption("image_count"),
createMandatoryNumberCriterionOption("gallery_count"), createMandatoryNumberCriterionOption("gallery_count"),
createMandatoryNumberCriterionOption("o_counter"),
createBooleanCriterionOption("ignore_auto_tag"), createBooleanCriterionOption("ignore_auto_tag"),
new NumberCriterionOption("height", "height_cm", "height_cm"), new NumberCriterionOption("height", "height_cm", "height_cm"),
...numberCriteria.map((c) => createNumberCriterionOption(c)), ...numberCriteria.map((c) => createNumberCriterionOption(c)),