Add studio performer count (#3362)

* Add studio performer count

* Add mocks
This commit is contained in:
DingDongSoLong4 2023-01-29 02:12:47 +02:00 committed by GitHub
parent c52d8c9314
commit 32e8496314
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 132 additions and 5 deletions

View File

@ -19,6 +19,7 @@ fragment StudioData on Studio {
scene_count
image_count
gallery_count
performer_count
movie_count
stash_ids {
stash_id

View File

@ -12,6 +12,7 @@ type Studio {
scene_count: Int # Resolver
image_count: Int # Resolver
gallery_count: Int # Resolver
performer_count: Int # Resolver
stash_ids: [StashID!]!
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")

View File

@ -9,6 +9,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"
)
func (r *studioResolver) Name(ctx context.Context, obj *models.Studio) (string, error) {
@ -93,6 +94,18 @@ func (r *studioResolver) GalleryCount(ctx context.Context, obj *models.Studio) (
return &res, nil
}
func (r *studioResolver) PerformerCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
var res int
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = performer.CountByStudioID(ctx, r.repository.Performer, obj.ID)
return err
}); err != nil {
return nil, err
}
return &res, nil
}
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) {
if !obj.ParentID.Valid {
return nil, nil

View File

@ -427,6 +427,27 @@ func (_m *PerformerReaderWriter) Query(ctx context.Context, performerFilter *mod
return r0, r1, r2
}
// QueryCount provides a mock function with given fields: ctx, galleryFilter, findFilter
func (_m *PerformerReaderWriter) QueryCount(ctx context.Context, galleryFilter *models.PerformerFilterType, findFilter *models.FindFilterType) (int, error) {
ret := _m.Called(ctx, galleryFilter, findFilter)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, *models.PerformerFilterType, *models.FindFilterType) int); ok {
r0 = rf(ctx, galleryFilter, findFilter)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *models.PerformerFilterType, *models.FindFilterType) error); ok {
r1 = rf(ctx, galleryFilter, findFilter)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// QueryForAutoTag provides a mock function with given fields: ctx, words
func (_m *PerformerReaderWriter) QueryForAutoTag(ctx context.Context, words []string) ([]*models.Performer, error) {
ret := _m.Called(ctx, words)

View File

@ -160,6 +160,7 @@ type PerformerReader interface {
// support the query needed
QueryForAutoTag(ctx context.Context, words []string) ([]*Performer, error)
Query(ctx context.Context, performerFilter *PerformerFilterType, findFilter *FindFilterType) ([]*Performer, int, error)
QueryCount(ctx context.Context, galleryFilter *PerformerFilterType, findFilter *FindFilterType) (int, error)
AliasLoader
GetImage(ctx context.Context, performerID int) ([]byte, error)
StashIDLoader

27
pkg/performer/query.go Normal file
View File

@ -0,0 +1,27 @@
package performer
import (
"context"
"strconv"
"github.com/stashapp/stash/pkg/models"
)
type Queryer interface {
Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error)
}
type CountQueryer interface {
QueryCount(ctx context.Context, galleryFilter *models.PerformerFilterType, findFilter *models.FindFilterType) (int, error)
}
func CountByStudioID(ctx context.Context, r CountQueryer, id int) (int, error) {
filter := &models.PerformerFilterType{
Studios: &models.HierarchicalMultiCriterionInput{
Value: []string{strconv.Itoa(id)},
Modifier: models.CriterionModifierIncludes,
},
}
return r.QueryCount(ctx, filter, nil)
}

View File

@ -617,7 +617,7 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform
return query
}
func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) {
func (qb *PerformerStore) makeQuery(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
if performerFilter == nil {
performerFilter = &models.PerformerFilterType{}
}
@ -635,13 +635,23 @@ func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.Per
}
if err := qb.validateFilter(performerFilter); err != nil {
return nil, 0, err
return nil, err
}
filter := qb.makeFilter(ctx, performerFilter)
query.addFilter(filter)
query.sortAndPagination = qb.getPerformerSort(findFilter) + getPagination(findFilter)
return &query, nil
}
func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) {
query, err := qb.makeQuery(ctx, performerFilter, findFilter)
if err != nil {
return nil, 0, err
}
idsResult, countResult, err := query.executeFind(ctx)
if err != nil {
return nil, 0, err
@ -655,6 +665,15 @@ func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.Per
return performers, countResult, nil
}
func (qb *PerformerStore) QueryCount(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) (int, error) {
query, err := qb.makeQuery(ctx, performerFilter, findFilter)
if err != nil {
return 0, err
}
return query.executeCount(ctx)
}
func performerIsMissingCriterionHandler(qb *PerformerStore, isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {

View File

@ -3,6 +3,7 @@ import {
faImage,
faImages,
faPlayCircle,
faUser,
} from "@fortawesome/free-solid-svg-icons";
import React, { useMemo } from "react";
import { Button } from "react-bootstrap";
@ -13,7 +14,7 @@ import { ConfigurationContext } from "src/hooks/Config";
import { TextUtils } from "src/utils";
import Icon from "./Icon";
type PopoverLinkType = "scene" | "image" | "gallery" | "movie";
type PopoverLinkType = "scene" | "image" | "gallery" | "movie" | "performer";
interface IProps {
className?: string;
@ -44,6 +45,8 @@ export const PopoverCountButton: React.FC<IProps> = ({
return faImages;
case "movie":
return faFilm;
case "performer":
return faUser;
}
}
@ -69,6 +72,11 @@ export const PopoverCountButton: React.FC<IProps> = ({
one: "movie",
other: "movies",
};
case "performer":
return {
one: "performer",
other: "performers",
};
}
}

View File

@ -116,12 +116,26 @@ export const StudioCard: React.FC<IProps> = ({
);
}
function maybeRenderPerformersPopoverButton() {
if (!studio.performer_count) return;
return (
<PopoverCountButton
className="performer-count"
type="performer"
count={studio.performer_count}
url={NavUtils.makeStudioPerformersUrl(studio)}
/>
);
}
function maybeRenderPopoverButtonGroup() {
if (
studio.scene_count ||
studio.image_count ||
studio.gallery_count ||
studio.movie_count
studio.movie_count ||
studio.performer_count
) {
return (
<>
@ -131,6 +145,7 @@ export const StudioCard: React.FC<IProps> = ({
{maybeRenderMoviesPopoverButton()}
{maybeRenderImagesPopoverButton()}
{maybeRenderGalleriesPopoverButton()}
{maybeRenderPerformersPopoverButton()}
</ButtonGroup>
</>
);

View File

@ -269,7 +269,15 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
</Tab>
<Tab
eventKey="performers"
title={intl.formatMessage({ id: "performers" })}
title={
<React.Fragment>
{intl.formatMessage({ id: "performers" })}
<Counter
abbreviateCounter={abbreviateCounter}
count={studio.performer_count ?? 0}
/>
</React.Fragment>
}
>
<StudioPerformersPanel studio={studio} />
</Tab>

View File

@ -148,6 +148,18 @@ const makeStudioMoviesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
return `/movies?${filter.makeQueryParameters()}`;
};
const makeStudioPerformersUrl = (studio: Partial<GQL.StudioDataFragment>) => {
if (!studio.id) return "#";
const filter = new ListFilterModel(GQL.FilterMode.Performers, undefined);
const criterion = new StudiosCriterion();
criterion.value = {
items: [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }],
depth: 0,
};
filter.criteria.push(criterion);
return `/performers?${filter.makeQueryParameters()}`;
};
const makeChildStudiosUrl = (studio: Partial<GQL.StudioDataFragment>) => {
if (!studio.id) return "#";
const filter = new ListFilterModel(GQL.FilterMode.Studios, undefined);
@ -307,6 +319,7 @@ export default {
makeStudioImagesUrl,
makeStudioGalleriesUrl,
makeStudioMoviesUrl,
makeStudioPerformersUrl,
makeParentTagsUrl,
makeChildTagsUrl,
makeTagSceneMarkersUrl,