mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Add performer pairings/appears with tab to performers (#3563)
* database query * Appears With panel * Typos * Validation fix * naming consistency, remove extraneous component. --------- Co-authored-by: kermieisinthehouse <kermie@isinthe.house>
This commit is contained in:
parent
64b7934af2
commit
3bc5caa6de
12 changed files with 187 additions and 8 deletions
|
|
@ -25,6 +25,7 @@ fragment PerformerData on Performer {
|
|||
image_count
|
||||
gallery_count
|
||||
movie_count
|
||||
performer_count
|
||||
o_counter
|
||||
|
||||
tags {
|
||||
|
|
|
|||
|
|
@ -118,6 +118,8 @@ input PerformerFilterType {
|
|||
death_year: IntCriterionInput
|
||||
"""Filter by studios where performer appears in scene/image/gallery"""
|
||||
studios: HierarchicalMultiCriterionInput
|
||||
"""Filter by performers where performer appears with another performer in scene/image/gallery"""
|
||||
performers: MultiCriterionInput
|
||||
"""Filter by autotag ignore value"""
|
||||
ignore_auto_tag: Boolean
|
||||
"""Filter by birthdate"""
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ type Performer {
|
|||
scene_count: Int # Resolver
|
||||
image_count: Int # Resolver
|
||||
gallery_count: Int # Resolver
|
||||
performer_count: Int # Resolver
|
||||
o_counter: Int # Resolver
|
||||
scenes: [Scene!]!
|
||||
stash_ids: [StashID!]!
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
)
|
||||
|
||||
// Checksum is deprecated
|
||||
|
|
@ -208,3 +209,15 @@ func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performe
|
|||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) PerformerCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = performer.CountByAppearsWith(ctx, r.repository.Performer, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,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 performers where performer appears with another performer in scene/image/gallery
|
||||
Performers *MultiCriterionInput `json:"performers"`
|
||||
// Filter by autotag ignore value
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
// Filter by birthdate
|
||||
|
|
|
|||
|
|
@ -25,3 +25,14 @@ func CountByStudioID(ctx context.Context, r CountQueryer, id int) (int, error) {
|
|||
|
||||
return r.QueryCount(ctx, filter, nil)
|
||||
}
|
||||
|
||||
func CountByAppearsWith(ctx context.Context, r CountQueryer, id int) (int, error) {
|
||||
filter := &models.PerformerFilterType{
|
||||
Performers: &models.MultiCriterionInput{
|
||||
Value: []string{strconv.Itoa(id)},
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
},
|
||||
}
|
||||
|
||||
return r.QueryCount(ctx, filter, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -625,6 +625,8 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform
|
|||
|
||||
query.handleCriterion(ctx, performerStudiosCriterionHandler(qb, filter.Studios))
|
||||
|
||||
query.handleCriterion(ctx, performerAppearsWithCriterionHandler(qb, filter.Performers))
|
||||
|
||||
query.handleCriterion(ctx, performerTagCountCriterionHandler(qb, filter.TagCount))
|
||||
query.handleCriterion(ctx, performerSceneCountCriterionHandler(qb, filter.SceneCount))
|
||||
query.handleCriterion(ctx, performerImageCountCriterionHandler(qb, filter.ImageCount))
|
||||
|
|
@ -899,6 +901,60 @@ func performerStudiosCriterionHandler(qb *PerformerStore, studios *models.Hierar
|
|||
}
|
||||
}
|
||||
|
||||
func performerAppearsWithCriterionHandler(qb *PerformerStore, performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performers != nil {
|
||||
formatMaps := []utils.StrFormatMap{
|
||||
{
|
||||
"primaryTable": performersScenesTable,
|
||||
"joinTable": performersScenesTable,
|
||||
"primaryFK": sceneIDColumn,
|
||||
},
|
||||
{
|
||||
"primaryTable": performersImagesTable,
|
||||
"joinTable": performersImagesTable,
|
||||
"primaryFK": imageIDColumn,
|
||||
},
|
||||
{
|
||||
"primaryTable": performersGalleriesTable,
|
||||
"joinTable": performersGalleriesTable,
|
||||
"primaryFK": galleryIDColumn,
|
||||
},
|
||||
}
|
||||
|
||||
if len(performers.Value) == '0' {
|
||||
return
|
||||
}
|
||||
|
||||
const derivedPerformerPerformersTable = "performer_performers"
|
||||
|
||||
valuesClause := strings.Join(performers.Value, "),(")
|
||||
|
||||
f.addWith("performer(id) AS (VALUES(" + valuesClause + "))")
|
||||
|
||||
templStr := `SELECT {primaryTable}2.performer_id FROM {primaryTable}
|
||||
INNER JOIN {primaryTable} AS {primaryTable}2 ON {primaryTable}.{primaryFK} = {primaryTable}2.{primaryFK}
|
||||
INNER JOIN performer ON {primaryTable}.performer_id = performer.id
|
||||
WHERE {primaryTable}2.performer_id != performer.id`
|
||||
|
||||
if performers.Modifier == models.CriterionModifierIncludesAll && len(performers.Value) > 1 {
|
||||
templStr += `
|
||||
GROUP BY {primaryTable}2.performer_id
|
||||
HAVING(count(distinct {primaryTable}.performer_id) IS ` + strconv.Itoa(len(performers.Value)) + `)`
|
||||
}
|
||||
|
||||
var unions []string
|
||||
for _, c := range formatMaps {
|
||||
unions = append(unions, utils.StrFormat(templStr, c))
|
||||
}
|
||||
|
||||
f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerPerformersTable, strings.Join(unions, " UNION ")))
|
||||
|
||||
f.addInnerJoin(derivedPerformerPerformersTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerPerformersTable))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) getPerformerSort(findFilter *models.FindFilterType) string {
|
||||
var sort string
|
||||
var direction string
|
||||
|
|
|
|||
|
|
@ -21,12 +21,14 @@ import { faHeart, faTag } from "@fortawesome/free-solid-svg-icons";
|
|||
import { RatingBanner } from "../Shared/RatingBanner";
|
||||
import cx from "classnames";
|
||||
import { usePerformerUpdate } from "src/core/StashService";
|
||||
import { ILabeledId } from "src/models/list-filter/types";
|
||||
|
||||
export interface IPerformerCardExtraCriteria {
|
||||
scenes: Criterion<CriterionValue>[];
|
||||
images: Criterion<CriterionValue>[];
|
||||
galleries: Criterion<CriterionValue>[];
|
||||
movies: Criterion<CriterionValue>[];
|
||||
scenes?: Criterion<CriterionValue>[];
|
||||
images?: Criterion<CriterionValue>[];
|
||||
galleries?: Criterion<CriterionValue>[];
|
||||
movies?: Criterion<CriterionValue>[];
|
||||
performer?: ILabeledId;
|
||||
}
|
||||
|
||||
interface IPerformerCardProps {
|
||||
|
|
@ -104,7 +106,11 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||
className="scene-count"
|
||||
type="scene"
|
||||
count={performer.scene_count}
|
||||
url={NavUtils.makePerformerScenesUrl(performer, extraCriteria?.scenes)}
|
||||
url={NavUtils.makePerformerScenesUrl(
|
||||
performer,
|
||||
extraCriteria?.performer,
|
||||
extraCriteria?.scenes
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -117,7 +123,11 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||
className="image-count"
|
||||
type="image"
|
||||
count={performer.image_count}
|
||||
url={NavUtils.makePerformerImagesUrl(performer, extraCriteria?.images)}
|
||||
url={NavUtils.makePerformerImagesUrl(
|
||||
performer,
|
||||
extraCriteria?.performer,
|
||||
extraCriteria?.images
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -132,6 +142,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||
count={performer.gallery_count}
|
||||
url={NavUtils.makePerformerGalleriesUrl(
|
||||
performer,
|
||||
extraCriteria?.performer,
|
||||
extraCriteria?.galleries
|
||||
)}
|
||||
/>
|
||||
|
|
@ -178,7 +189,11 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||
className="movie-count"
|
||||
type="movie"
|
||||
count={performer.movie_count}
|
||||
url={NavUtils.makePerformerMoviesUrl(performer, extraCriteria?.movies)}
|
||||
url={NavUtils.makePerformerMoviesUrl(
|
||||
performer,
|
||||
extraCriteria?.performer,
|
||||
extraCriteria?.movies
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
|||
import { PerformerGalleriesPanel } from "./PerformerGalleriesPanel";
|
||||
import { PerformerMoviesPanel } from "./PerformerMoviesPanel";
|
||||
import { PerformerImagesPanel } from "./PerformerImagesPanel";
|
||||
import { PerformerAppearsWithPanel } from "./performerAppearsWithPanel";
|
||||
import { PerformerEditPanel } from "./PerformerEditPanel";
|
||||
import { PerformerSubmitButton } from "./PerformerSubmitButton";
|
||||
import GenderIcon from "../GenderIcon";
|
||||
|
|
@ -93,7 +94,8 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||
tab === "scenes" ||
|
||||
tab === "galleries" ||
|
||||
tab === "images" ||
|
||||
tab === "movies"
|
||||
tab === "movies" ||
|
||||
tab == "appearswith"
|
||||
? tab
|
||||
: "details";
|
||||
const setActiveTabKey = (newTab: string | null) => {
|
||||
|
|
@ -263,6 +265,23 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||
performer={performer}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="appearswith"
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "appears_with" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={performer.performer_count ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
<PerformerAppearsWithPanel
|
||||
active={activeTabKey == "appearswith"}
|
||||
performer={performer}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
import React from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { PerformerList } from "src/components/Performers/PerformerList";
|
||||
import { usePerformerFilterHook } from "src/core/performers";
|
||||
|
||||
interface IPerformerDetailsProps {
|
||||
active: boolean;
|
||||
performer: GQL.PerformerDataFragment;
|
||||
}
|
||||
|
||||
export const PerformerAppearsWithPanel: React.FC<IPerformerDetailsProps> = ({
|
||||
active,
|
||||
performer,
|
||||
}) => {
|
||||
const performerValue = {
|
||||
id: performer.id,
|
||||
label: performer.name ?? `Performer ${performer.id}`,
|
||||
};
|
||||
|
||||
const extraCriteria = {
|
||||
performer: performerValue,
|
||||
};
|
||||
|
||||
const filterHook = usePerformerFilterHook(performer);
|
||||
|
||||
return (
|
||||
<PerformerList
|
||||
filterHook={filterHook}
|
||||
extraCriteria={extraCriteria}
|
||||
alterQuery={active}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -127,6 +127,7 @@
|
|||
"aliases": "Aliases",
|
||||
"all": "all",
|
||||
"also_known_as": "Also known as",
|
||||
"appears_with": "Appears With",
|
||||
"ascending": "Ascending",
|
||||
"average_resolution": "Average Resolution",
|
||||
"between_and": "and",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from "src/models/list-filter/criteria/criterion";
|
||||
import { GalleriesCriterion } from "src/models/list-filter/criteria/galleries";
|
||||
import { PhashCriterion } from "src/models/list-filter/criteria/phash";
|
||||
import { ILabeledId } from "src/models/list-filter/types";
|
||||
|
||||
function addExtraCriteria(
|
||||
dest: Criterion<CriterionValue>[],
|
||||
|
|
@ -31,6 +32,7 @@ function addExtraCriteria(
|
|||
|
||||
const makePerformerScenesUrl = (
|
||||
performer: Partial<GQL.PerformerDataFragment>,
|
||||
extraPerformer?: ILabeledId,
|
||||
extraCriteria?: Criterion<CriterionValue>[]
|
||||
) => {
|
||||
if (!performer.id) return "#";
|
||||
|
|
@ -39,6 +41,11 @@ const makePerformerScenesUrl = (
|
|||
criterion.value = [
|
||||
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
|
||||
];
|
||||
|
||||
if (extraPerformer) {
|
||||
criterion.value.push(extraPerformer);
|
||||
}
|
||||
|
||||
filter.criteria.push(criterion);
|
||||
addExtraCriteria(filter.criteria, extraCriteria);
|
||||
return `/scenes?${filter.makeQueryParameters()}`;
|
||||
|
|
@ -46,6 +53,7 @@ const makePerformerScenesUrl = (
|
|||
|
||||
const makePerformerImagesUrl = (
|
||||
performer: Partial<GQL.PerformerDataFragment>,
|
||||
extraPerformer?: ILabeledId,
|
||||
extraCriteria?: Criterion<CriterionValue>[]
|
||||
) => {
|
||||
if (!performer.id) return "#";
|
||||
|
|
@ -54,6 +62,11 @@ const makePerformerImagesUrl = (
|
|||
criterion.value = [
|
||||
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
|
||||
];
|
||||
|
||||
if (extraPerformer) {
|
||||
criterion.value.push(extraPerformer);
|
||||
}
|
||||
|
||||
filter.criteria.push(criterion);
|
||||
addExtraCriteria(filter.criteria, extraCriteria);
|
||||
return `/images?${filter.makeQueryParameters()}`;
|
||||
|
|
@ -61,6 +74,7 @@ const makePerformerImagesUrl = (
|
|||
|
||||
const makePerformerGalleriesUrl = (
|
||||
performer: Partial<GQL.PerformerDataFragment>,
|
||||
extraPerformer?: ILabeledId,
|
||||
extraCriteria?: Criterion<CriterionValue>[]
|
||||
) => {
|
||||
if (!performer.id) return "#";
|
||||
|
|
@ -69,6 +83,11 @@ const makePerformerGalleriesUrl = (
|
|||
criterion.value = [
|
||||
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
|
||||
];
|
||||
|
||||
if (extraPerformer) {
|
||||
criterion.value.push(extraPerformer);
|
||||
}
|
||||
|
||||
filter.criteria.push(criterion);
|
||||
addExtraCriteria(filter.criteria, extraCriteria);
|
||||
return `/galleries?${filter.makeQueryParameters()}`;
|
||||
|
|
@ -76,6 +95,7 @@ const makePerformerGalleriesUrl = (
|
|||
|
||||
const makePerformerMoviesUrl = (
|
||||
performer: Partial<GQL.PerformerDataFragment>,
|
||||
extraPerformer?: ILabeledId,
|
||||
extraCriteria?: Criterion<CriterionValue>[]
|
||||
) => {
|
||||
if (!performer.id) return "#";
|
||||
|
|
@ -84,6 +104,11 @@ const makePerformerMoviesUrl = (
|
|||
criterion.value = [
|
||||
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
|
||||
];
|
||||
|
||||
if (extraPerformer) {
|
||||
criterion.value.push(extraPerformer);
|
||||
}
|
||||
|
||||
filter.criteria.push(criterion);
|
||||
addExtraCriteria(filter.criteria, extraCriteria);
|
||||
return `/movies?${filter.makeQueryParameters()}`;
|
||||
|
|
|
|||
Loading…
Reference in a new issue