Group O-Counter Filter/Sort (#6122)

This commit is contained in:
EventHoriizon 2025-11-10 00:53:53 +00:00 committed by GitHub
parent 2e766952dd
commit d5b1046267
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 128 additions and 0 deletions

View file

@ -403,6 +403,8 @@ input GroupFilterType {
created_at: TimestampCriterionInput created_at: TimestampCriterionInput
"Filter by last update time" "Filter by last update time"
updated_at: TimestampCriterionInput updated_at: TimestampCriterionInput
"Filter by o-counter"
o_counter: IntCriterionInput
"Filter by containing groups" "Filter by containing groups"
containing_groups: HierarchicalMultiCriterionInput containing_groups: HierarchicalMultiCriterionInput

View file

@ -30,6 +30,7 @@ type Group {
performer_count(depth: Int): Int! # Resolver performer_count(depth: Int): Int! # Resolver
sub_group_count(depth: Int): Int! # Resolver sub_group_count(depth: Int): Int! # Resolver
scenes: [Scene!]! scenes: [Scene!]!
o_counter: Int # Resolver
} }
input GroupDescriptionInput { input GroupDescriptionInput {

View file

@ -204,3 +204,14 @@ func (r *groupResolver) Scenes(ctx context.Context, obj *models.Group) (ret []*m
return ret, nil return ret, nil
} }
func (r *groupResolver) OCounter(ctx context.Context, obj *models.Group) (ret *int, err error) {
var count int
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
count, err = r.repository.Scene.OCountByGroupID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
}
return &count, nil
}

View file

@ -23,6 +23,8 @@ type GroupFilterType struct {
TagCount *IntCriterionInput `json:"tag_count"` TagCount *IntCriterionInput `json:"tag_count"`
// Filter by date // Filter by date
Date *DateCriterionInput `json:"date"` Date *DateCriterionInput `json:"date"`
// Filter by O counter
OCounter *IntCriterionInput `json:"o_counter"`
// Filter by containing groups // Filter by containing groups
ContainingGroups *HierarchicalMultiCriterionInput `json:"containing_groups"` ContainingGroups *HierarchicalMultiCriterionInput `json:"containing_groups"`
// Filter by sub groups // Filter by sub groups

View file

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

View file

@ -44,6 +44,7 @@ type SceneCounter interface {
CountMissingChecksum(ctx context.Context) (int, error) CountMissingChecksum(ctx context.Context) (int, error)
CountMissingOSHash(ctx context.Context) (int, error) CountMissingOSHash(ctx context.Context) (int, error)
OCountByPerformerID(ctx context.Context, performerID int) (int, error) OCountByPerformerID(ctx context.Context, performerID int) (int, error)
OCountByGroupID(ctx context.Context, groupID int) (int, error)
} }
// SceneCreator provides methods to create scenes. // SceneCreator provides methods to create scenes.

View file

@ -488,6 +488,7 @@ var groupSortOptions = sortOptions{
"random", "random",
"rating", "rating",
"scenes_count", "scenes_count",
"o_counter",
"sub_group_order", "sub_group_order",
"tag_count", "tag_count",
"updated_at", "updated_at",
@ -524,6 +525,8 @@ func (qb *GroupStore) setGroupSort(query *queryBuilder, findFilter *models.FindF
query.sortAndPagination += getCountSort(groupTable, groupsTagsTable, groupIDColumn, direction) query.sortAndPagination += getCountSort(groupTable, groupsTagsTable, groupIDColumn, direction)
case "scenes_count": // generic getSort won't work for this case "scenes_count": // generic getSort won't work for this
query.sortAndPagination += getCountSort(groupTable, groupsScenesTable, groupIDColumn, direction) query.sortAndPagination += getCountSort(groupTable, groupsScenesTable, groupIDColumn, direction)
case "o_counter":
query.sortAndPagination += qb.sortByOCounter(direction)
default: default:
query.sortAndPagination += getSort(sort, direction, "groups") query.sortAndPagination += getSort(sort, direction, "groups")
} }
@ -701,3 +704,8 @@ func (qb *GroupStore) FindInAncestors(ctx context.Context, ascestorIDs []int, id
return ret, nil return ret, nil
} }
func (qb *GroupStore) sortByOCounter(direction string) string {
// need to sum the o_counter from scenes and images
return " ORDER BY (" + selectGroupOCountSQL + ") " + direction
}

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
) )
type groupFilterHandler struct { type groupFilterHandler struct {
@ -73,6 +74,7 @@ func (qb *groupFilterHandler) criterionHandler() criterionHandler {
qb.performersCriterionHandler(groupFilter.Performers), qb.performersCriterionHandler(groupFilter.Performers),
qb.tagsCriterionHandler(groupFilter.Tags), qb.tagsCriterionHandler(groupFilter.Tags),
qb.tagCountCriterionHandler(groupFilter.TagCount), qb.tagCountCriterionHandler(groupFilter.TagCount),
qb.groupOCounterCriterionHandler(groupFilter.OCounter),
&dateCriterionHandler{groupFilter.Date, "groups.date", nil}, &dateCriterionHandler{groupFilter.Date, "groups.date", nil},
groupHierarchyHandler.ParentsCriterionHandler(groupFilter.ContainingGroups), groupHierarchyHandler.ParentsCriterionHandler(groupFilter.ContainingGroups),
groupHierarchyHandler.ChildrenCriterionHandler(groupFilter.SubGroups), groupHierarchyHandler.ChildrenCriterionHandler(groupFilter.SubGroups),
@ -201,3 +203,37 @@ func (qb *groupFilterHandler) tagCountCriterionHandler(count *models.IntCriterio
return h.handler(count) return h.handler(count)
} }
// used for sorting and filtering on group o-count
var selectGroupOCountSQL = utils.StrFormat(
"SELECT SUM(o_counter) "+
"FROM ("+
"SELECT COUNT({scenes_o_dates}.{o_date}) as o_counter from {groups_scenes} s "+
"LEFT JOIN {scenes} ON {scenes}.id = s.{scene_id} "+
"LEFT JOIN {scenes_o_dates} ON {scenes_o_dates}.{scene_id} = {scenes}.id "+
"WHERE s.{group_id} = {group}.id "+
")",
map[string]interface{}{
"group": groupTable,
"group_id": groupIDColumn,
"groups_scenes": groupsScenesTable,
"scenes": sceneTable,
"scene_id": sceneIDColumn,
"scenes_o_dates": scenesODatesTable,
"o_date": sceneODateColumn,
},
)
func (qb *groupFilterHandler) groupOCounterCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if count == nil {
return
}
lhs := "(" + selectGroupOCountSQL + ")"
clause, args := getIntCriterionWhereClause(lhs, *count)
f.addWhere(clause, args...)
}
}

View file

@ -795,6 +795,29 @@ func (qb *SceneStore) OCountByPerformerID(ctx context.Context, performerID int)
return ret, nil return ret, nil
} }
func (qb *SceneStore) OCountByGroupID(ctx context.Context, groupID int) (int, error) {
table := qb.table()
joinTable := scenesGroupsJoinTable
oHistoryTable := goqu.T(scenesODatesTable)
q := dialect.Select(goqu.COUNT("*")).From(table).InnerJoin(
oHistoryTable,
goqu.On(table.Col(idColumn).Eq(oHistoryTable.Col(sceneIDColumn))),
).InnerJoin(
joinTable,
goqu.On(
table.Col(idColumn).Eq(joinTable.Col(sceneIDColumn)),
),
).Where(joinTable.Col(groupIDColumn).Eq(groupID))
var ret int
if err := querySimple(ctx, q, &ret); err != nil {
return 0, err
}
return ret, nil
}
func (qb *SceneStore) FindByGroupID(ctx context.Context, groupID int) ([]*models.Scene, error) { func (qb *SceneStore) FindByGroupID(ctx context.Context, groupID int) ([]*models.Scene, error) {
sq := dialect.From(scenesGroupsJoinTable).Select(scenesGroupsJoinTable.Col(sceneIDColumn)).Where( sq := dialect.From(scenesGroupsJoinTable).Select(scenesGroupsJoinTable.Col(sceneIDColumn)).Where(
scenesGroupsJoinTable.Col(groupIDColumn).Eq(groupID), scenesGroupsJoinTable.Col(groupIDColumn).Eq(groupID),

View file

@ -32,6 +32,7 @@ fragment GroupData on Group {
performer_count_all: performer_count(depth: -1) performer_count_all: performer_count(depth: -1)
sub_group_count sub_group_count
sub_group_count_all: sub_group_count(depth: -1) sub_group_count_all: sub_group_count(depth: -1)
o_counter
scenes { scenes {
id id

View file

@ -10,6 +10,7 @@ import { FormattedMessage } from "react-intl";
import { RatingBanner } from "../Shared/RatingBanner"; import { RatingBanner } from "../Shared/RatingBanner";
import { faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons"; import { faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons";
import { RelatedGroupPopoverButton } from "./RelatedGroupPopover"; import { RelatedGroupPopoverButton } from "./RelatedGroupPopover";
import { SweatDrops } from "../Shared/SweatDrops";
const Description: React.FC<{ const Description: React.FC<{
sceneNumber?: number; sceneNumber?: number;
@ -107,6 +108,21 @@ export const GroupCard: React.FC<IProps> = ({
); );
} }
function maybeRenderOCounter() {
if (!group.o_counter) return;
return (
<div className="o-counter">
<Button className="minimal">
<span className="fa-icon">
<SweatDrops />
</span>
<span>{group.o_counter}</span>
</Button>
</div>
);
}
function maybeRenderPopoverButtonGroup() { function maybeRenderPopoverButtonGroup() {
if ( if (
sceneNumber || sceneNumber ||
@ -130,6 +146,7 @@ export const GroupCard: React.FC<IProps> = ({
group.containing_groups.length > 0) && ( group.containing_groups.length > 0) && (
<RelatedGroupPopoverButton group={group} /> <RelatedGroupPopoverButton group={group} />
)} )}
{maybeRenderOCounter()}
</ButtonGroup> </ButtonGroup>
</> </>
); );

View file

@ -35,6 +35,10 @@ const sortByOptions = [
messageID: "scene_count", messageID: "scene_count",
value: "scenes_count", value: "scenes_count",
}, },
{
messageID: "o_count",
value: "o_counter",
},
]); ]);
const displayModeOptions = [DisplayMode.Grid]; const displayModeOptions = [DisplayMode.Grid];
const criterionOptions = [ const criterionOptions = [
@ -49,6 +53,7 @@ const criterionOptions = [
RatingCriterionOption, RatingCriterionOption,
PerformersCriterionOption, PerformersCriterionOption,
createDateCriterionOption("date"), createDateCriterionOption("date"),
createMandatoryNumberCriterionOption("o_counter", "o_count"),
ContainingGroupsCriterionOption, ContainingGroupsCriterionOption,
SubGroupsCriterionOption, SubGroupsCriterionOption,
createMandatoryNumberCriterionOption("containing_group_count"), createMandatoryNumberCriterionOption("containing_group_count"),