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
gallery_count
movie_count
o_counter
tags {
...SlimTagData

View File

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

View File

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

View File

@@ -127,6 +127,24 @@ func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Perfor
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) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
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)
FindByGalleryID(ctx context.Context, galleryID int) ([]*Image, error)
CountByGalleryID(ctx context.Context, galleryID int) (int, error)
OCountByPerformerID(ctx context.Context, performerID int) (int, error)
Count(ctx context.Context) (int, error)
Size(ctx context.Context) (float64, 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
}
// 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
func (_m *ImageReaderWriter) Create(ctx context.Context, newImage *models.ImageCreateInput) error {
ret := _m.Called(ctx, newImage)

View File

@@ -102,6 +102,27 @@ func (_m *SceneReaderWriter) CountByPerformerID(ctx context.Context, performerID
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
func (_m *SceneReaderWriter) CountByStudioID(ctx context.Context, studioID int) (int, error) {
ret := _m.Called(ctx, studioID)

View File

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

View File

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

View File

@@ -332,6 +332,7 @@ type Performer struct {
Deleted bool `json:"deleted"`
Edits []*Edit `json:"edits,omitempty"`
SceneCount int `json:"scene_count"`
OCounter int `json:"o_counter"`
MergedIds []string `json:"merged_ids,omitempty"`
Studios []*PerformerStudio `json:"studios,omitempty"`
IsFavorite bool `json:"is_favorite"`
@@ -1771,6 +1772,7 @@ const (
PerformerSortEnumName PerformerSortEnum = "NAME"
PerformerSortEnumBirthdate PerformerSortEnum = "BIRTHDATE"
PerformerSortEnumSceneCount PerformerSortEnum = "SCENE_COUNT"
PerformerSortEnumOCounter PerformerSortEnum = "O_COUNTER"
PerformerSortEnumCareerStartYear PerformerSortEnum = "CAREER_START_YEAR"
PerformerSortEnumDebut PerformerSortEnum = "DEBUT"
PerformerSortEnumCreatedAt PerformerSortEnum = "CREATED_AT"
@@ -1781,6 +1783,7 @@ var AllPerformerSortEnum = []PerformerSortEnum{
PerformerSortEnumName,
PerformerSortEnumBirthdate,
PerformerSortEnumSceneCount,
PerformerSortEnumOCounter,
PerformerSortEnumCareerStartYear,
PerformerSortEnumDebut,
PerformerSortEnumCreatedAt,
@@ -1789,7 +1792,7 @@ var AllPerformerSortEnum = []PerformerSortEnum{
func (e PerformerSortEnum) IsValid() bool {
switch e {
case PerformerSortEnumName, PerformerSortEnumBirthdate, PerformerSortEnumSceneCount, PerformerSortEnumCareerStartYear, PerformerSortEnumDebut, PerformerSortEnumCreatedAt, PerformerSortEnumUpdatedAt:
case PerformerSortEnumName, PerformerSortEnumBirthdate, PerformerSortEnumSceneCount, PerformerSortEnumOCounter, PerformerSortEnumCareerStartYear, PerformerSortEnumDebut, PerformerSortEnumCreatedAt, PerformerSortEnumUpdatedAt:
return true
}
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
type stringListCriterionHandlerBuilder struct {
// 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)
}
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) {
table := qb.table()
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, performerImageCountCriterionHandler(qb, filter.ImageCount))
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.DeathDate, tableName+".death_date"))
query.handleCriterion(ctx, timestampCriterionHandler(filter.CreatedAt, tableName+".created_at"))
@@ -805,6 +806,22 @@ func performerGalleryCountCriterionHandler(qb *PerformerStore, count *models.Int
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 {
return func(ctx context.Context, f *filterBuilder) {
if studios != nil {
@@ -906,6 +923,9 @@ func (qb *PerformerStore) getPerformerSort(findFilter *models.FindFilterType) st
default:
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
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)
}
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) {
sq := dialect.From(scenesMoviesJoinTable).Select(scenesMoviesJoinTable.Col(sceneIDColumn)).Where(
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))
}
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 {
var likeClauses []string
var args []interface{}
@@ -287,6 +308,28 @@ func getCountCriterionClause(primaryTable, joinTable, primaryFK string, criterio
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 {
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 { GridCard } from "../Shared/GridCard";
import { CountryFlag } from "../Shared/CountryFlag";
import { SweatDrops } from "../Shared/SweatDrops";
import { HoverPopover } from "../Shared/HoverPopover";
import { Icon } from "../Shared/Icon";
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() {
if (performer.tags.length <= 0) return;
@@ -173,6 +189,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
performer.image_count ||
performer.gallery_count ||
performer.tags.length > 0 ||
performer.o_counter ||
performer.movie_count
) {
return (
@@ -184,6 +201,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
{maybeRenderImagesPopoverButton()}
{maybeRenderGalleriesPopoverButton()}
{maybeRenderTagPopoverButton()}
{maybeRenderOCounter()}
</ButtonGroup>
</>
);

View File

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

View File

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