mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Group O-Counter Filter/Sort (#6122)
This commit is contained in:
parent
2e766952dd
commit
d5b1046267
12 changed files with 128 additions and 0 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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...)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue