mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
feat: Add Performers tab to Group detail page (#5895)
* Feat(#1401): Show all performers from group's scenes on group detail * Add Groups criterion to performers --------- Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -178,6 +178,8 @@ type PerformerFilterType struct {
|
||||
DeathYear *IntCriterionInput `json:"death_year"`
|
||||
// Filter by studios where performer appears in scene/image/gallery
|
||||
Studios *HierarchicalMultiCriterionInput `json:"studios"`
|
||||
// Filter by groups where performer appears in scene
|
||||
Groups *HierarchicalMultiCriterionInput `json:"groups"`
|
||||
// Filter by performers where performer appears with another performer in scene/image/gallery
|
||||
Performers *MultiCriterionInput `json:"performers"`
|
||||
// Filter by autotag ignore value
|
||||
|
||||
@@ -19,6 +19,18 @@ func CountByStudioID(ctx context.Context, r models.PerformerQueryer, id int, dep
|
||||
return r.QueryCount(ctx, filter, nil)
|
||||
}
|
||||
|
||||
func CountByGroupID(ctx context.Context, r models.PerformerQueryer, id int, depth *int) (int, error) {
|
||||
filter := &models.PerformerFilterType{
|
||||
Groups: &models.HierarchicalMultiCriterionInput{
|
||||
Value: []string{strconv.Itoa(id)},
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
Depth: depth,
|
||||
},
|
||||
}
|
||||
|
||||
return r.QueryCount(ctx, filter, nil)
|
||||
}
|
||||
|
||||
func CountByTagID(ctx context.Context, r models.PerformerQueryer, id int, depth *int) (int, error) {
|
||||
filter := &models.PerformerFilterType{
|
||||
Tags: &models.HierarchicalMultiCriterionInput{
|
||||
|
||||
@@ -155,6 +155,8 @@ func (qb *performerFilterHandler) criterionHandler() criterionHandler {
|
||||
|
||||
qb.studiosCriterionHandler(filter.Studios),
|
||||
|
||||
qb.groupsCriterionHandler(filter.Groups),
|
||||
|
||||
qb.appearsWithCriterionHandler(filter.Performers),
|
||||
|
||||
qb.tagCountCriterionHandler(filter.TagCount),
|
||||
@@ -487,6 +489,119 @@ func (qb *performerFilterHandler) studiosCriterionHandler(studios *models.Hierar
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) groupsCriterionHandler(groups *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if groups != nil {
|
||||
if groups.Modifier == models.CriterionModifierIsNull || groups.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if groups.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
f.addLeftJoin(performersScenesTable, "", "performers_scenes.performer_id = performers.id")
|
||||
f.addLeftJoin(groupsScenesTable, "", "performers_scenes.scene_id = groups_scenes.scene_id")
|
||||
|
||||
f.addWhere(fmt.Sprintf("%s groups_scenes.group_id IS NULL", notClause))
|
||||
return
|
||||
}
|
||||
|
||||
if len(groups.Value) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var clauseCondition string
|
||||
|
||||
switch groups.Modifier {
|
||||
case models.CriterionModifierIncludes:
|
||||
// return performers who appear in scenes with any of the given groups
|
||||
clauseCondition = "NOT"
|
||||
case models.CriterionModifierExcludes:
|
||||
// exclude performers who appear in scenes with any of the given groups
|
||||
clauseCondition = ""
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
const derivedPerformerGroupTable = "performer_group"
|
||||
|
||||
// Simplified approach: direct group-scene-performer relationship without hierarchy
|
||||
var args []interface{}
|
||||
for _, val := range groups.Value {
|
||||
args = append(args, val)
|
||||
}
|
||||
|
||||
// If depth is specified and not 0, we need hierarchy, otherwise use simple approach
|
||||
depthVal := 0
|
||||
if groups.Depth != nil {
|
||||
depthVal = *groups.Depth
|
||||
}
|
||||
|
||||
if depthVal == 0 {
|
||||
// Simple case: no hierarchy, direct group relationship
|
||||
f.addWith(fmt.Sprintf("group_values(id) AS (VALUES %s)", strings.Repeat("(?),", len(groups.Value)-1)+"(?)"), args...)
|
||||
|
||||
templStr := `SELECT performer_id FROM {joinTable}
|
||||
INNER JOIN {primaryTable} ON {joinTable}.scene_id = {primaryTable}.scene_id
|
||||
INNER JOIN group_values ON {primaryTable}.{groupFK} = group_values.id`
|
||||
|
||||
formatMaps := []utils.StrFormatMap{
|
||||
{
|
||||
"primaryTable": groupsScenesTable,
|
||||
"joinTable": performersScenesTable,
|
||||
"primaryFK": sceneIDColumn,
|
||||
"groupFK": groupIDColumn,
|
||||
},
|
||||
}
|
||||
|
||||
var unions []string
|
||||
for _, c := range formatMaps {
|
||||
unions = append(unions, utils.StrFormat(templStr, c))
|
||||
}
|
||||
|
||||
f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerGroupTable, strings.Join(unions, " UNION ")))
|
||||
} else {
|
||||
// Complex case: with hierarchy
|
||||
var depthCondition string
|
||||
if depthVal != -1 {
|
||||
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
|
||||
}
|
||||
|
||||
// Build recursive CTE for group hierarchy
|
||||
hierarchyQuery := fmt.Sprintf(`group_hierarchy AS (
|
||||
SELECT sub_id AS root_id, sub_id AS item_id, 0 AS depth FROM groups_relations WHERE sub_id IN%s
|
||||
UNION
|
||||
SELECT root_id, sub_id, depth + 1 FROM groups_relations INNER JOIN group_hierarchy ON item_id = containing_id %s
|
||||
)`, getInBinding(len(groups.Value)), depthCondition)
|
||||
|
||||
f.addRecursiveWith(hierarchyQuery, args...)
|
||||
|
||||
templStr := `SELECT performer_id FROM {joinTable}
|
||||
INNER JOIN {primaryTable} ON {joinTable}.scene_id = {primaryTable}.scene_id
|
||||
INNER JOIN group_hierarchy ON {primaryTable}.{groupFK} = group_hierarchy.item_id`
|
||||
|
||||
formatMaps := []utils.StrFormatMap{
|
||||
{
|
||||
"primaryTable": groupsScenesTable,
|
||||
"joinTable": performersScenesTable,
|
||||
"primaryFK": sceneIDColumn,
|
||||
"groupFK": groupIDColumn,
|
||||
},
|
||||
}
|
||||
|
||||
var unions []string
|
||||
for _, c := range formatMaps {
|
||||
unions = append(unions, utils.StrFormat(templStr, c))
|
||||
}
|
||||
|
||||
f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerGroupTable, strings.Join(unions, " UNION ")))
|
||||
}
|
||||
|
||||
f.addLeftJoin(derivedPerformerGroupTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerGroupTable))
|
||||
f.addWhere(fmt.Sprintf("%s.performer_id IS %s NULL", derivedPerformerGroupTable, clauseCondition))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) appearsWithCriterionHandler(performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performers != nil {
|
||||
|
||||
Reference in New Issue
Block a user