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:
philMorel
2025-06-11 16:07:09 +09:00
committed by GitHub
parent 3d03072da0
commit 60f1ee2360
12 changed files with 237 additions and 5 deletions

View File

@@ -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

View File

@@ -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{

View File

@@ -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 {