mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
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:
@@ -25,6 +25,7 @@ fragment PerformerData on Performer {
|
||||
image_count
|
||||
gallery_count
|
||||
movie_count
|
||||
o_counter
|
||||
|
||||
tags {
|
||||
...SlimTagData
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user