mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Add studio performer count (#3362)
* Add studio performer count * Add mocks
This commit is contained in:
parent
c52d8c9314
commit
32e8496314
11 changed files with 132 additions and 5 deletions
|
|
@ -19,6 +19,7 @@ fragment StudioData on Studio {
|
||||||
scene_count
|
scene_count
|
||||||
image_count
|
image_count
|
||||||
gallery_count
|
gallery_count
|
||||||
|
performer_count
|
||||||
movie_count
|
movie_count
|
||||||
stash_ids {
|
stash_ids {
|
||||||
stash_id
|
stash_id
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ type Studio {
|
||||||
scene_count: Int # Resolver
|
scene_count: Int # Resolver
|
||||||
image_count: Int # Resolver
|
image_count: Int # Resolver
|
||||||
gallery_count: Int # Resolver
|
gallery_count: Int # Resolver
|
||||||
|
performer_count: Int # Resolver
|
||||||
stash_ids: [StashID!]!
|
stash_ids: [StashID!]!
|
||||||
# rating expressed as 1-5
|
# rating expressed as 1-5
|
||||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/gallery"
|
"github.com/stashapp/stash/pkg/gallery"
|
||||||
"github.com/stashapp/stash/pkg/image"
|
"github.com/stashapp/stash/pkg/image"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/performer"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *studioResolver) Name(ctx context.Context, obj *models.Studio) (string, error) {
|
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
|
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) {
|
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) {
|
||||||
if !obj.ParentID.Valid {
|
if !obj.ParentID.Valid {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
|
||||||
|
|
@ -427,6 +427,27 @@ func (_m *PerformerReaderWriter) Query(ctx context.Context, performerFilter *mod
|
||||||
return r0, r1, r2
|
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
|
// QueryForAutoTag provides a mock function with given fields: ctx, words
|
||||||
func (_m *PerformerReaderWriter) QueryForAutoTag(ctx context.Context, words []string) ([]*models.Performer, error) {
|
func (_m *PerformerReaderWriter) QueryForAutoTag(ctx context.Context, words []string) ([]*models.Performer, error) {
|
||||||
ret := _m.Called(ctx, words)
|
ret := _m.Called(ctx, words)
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,7 @@ type PerformerReader interface {
|
||||||
// support the query needed
|
// support the query needed
|
||||||
QueryForAutoTag(ctx context.Context, words []string) ([]*Performer, error)
|
QueryForAutoTag(ctx context.Context, words []string) ([]*Performer, error)
|
||||||
Query(ctx context.Context, performerFilter *PerformerFilterType, findFilter *FindFilterType) ([]*Performer, int, error)
|
Query(ctx context.Context, performerFilter *PerformerFilterType, findFilter *FindFilterType) ([]*Performer, int, error)
|
||||||
|
QueryCount(ctx context.Context, galleryFilter *PerformerFilterType, findFilter *FindFilterType) (int, error)
|
||||||
AliasLoader
|
AliasLoader
|
||||||
GetImage(ctx context.Context, performerID int) ([]byte, error)
|
GetImage(ctx context.Context, performerID int) ([]byte, error)
|
||||||
StashIDLoader
|
StashIDLoader
|
||||||
|
|
|
||||||
27
pkg/performer/query.go
Normal file
27
pkg/performer/query.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -617,7 +617,7 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform
|
||||||
return query
|
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 {
|
if performerFilter == nil {
|
||||||
performerFilter = &models.PerformerFilterType{}
|
performerFilter = &models.PerformerFilterType{}
|
||||||
}
|
}
|
||||||
|
|
@ -635,13 +635,23 @@ func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.Per
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := qb.validateFilter(performerFilter); err != nil {
|
if err := qb.validateFilter(performerFilter); err != nil {
|
||||||
return nil, 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
filter := qb.makeFilter(ctx, performerFilter)
|
filter := qb.makeFilter(ctx, performerFilter)
|
||||||
|
|
||||||
query.addFilter(filter)
|
query.addFilter(filter)
|
||||||
|
|
||||||
query.sortAndPagination = qb.getPerformerSort(findFilter) + getPagination(findFilter)
|
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)
|
idsResult, countResult, err := query.executeFind(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
|
@ -655,6 +665,15 @@ func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.Per
|
||||||
return performers, countResult, nil
|
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 {
|
func performerIsMissingCriterionHandler(qb *PerformerStore, isMissing *string) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if isMissing != nil && *isMissing != "" {
|
if isMissing != nil && *isMissing != "" {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
faImage,
|
faImage,
|
||||||
faImages,
|
faImages,
|
||||||
faPlayCircle,
|
faPlayCircle,
|
||||||
|
faUser,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
|
|
@ -13,7 +14,7 @@ import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
|
||||||
type PopoverLinkType = "scene" | "image" | "gallery" | "movie";
|
type PopoverLinkType = "scene" | "image" | "gallery" | "movie" | "performer";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -44,6 +45,8 @@ export const PopoverCountButton: React.FC<IProps> = ({
|
||||||
return faImages;
|
return faImages;
|
||||||
case "movie":
|
case "movie":
|
||||||
return faFilm;
|
return faFilm;
|
||||||
|
case "performer":
|
||||||
|
return faUser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,6 +72,11 @@ export const PopoverCountButton: React.FC<IProps> = ({
|
||||||
one: "movie",
|
one: "movie",
|
||||||
other: "movies",
|
other: "movies",
|
||||||
};
|
};
|
||||||
|
case "performer":
|
||||||
|
return {
|
||||||
|
one: "performer",
|
||||||
|
other: "performers",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
function maybeRenderPopoverButtonGroup() {
|
||||||
if (
|
if (
|
||||||
studio.scene_count ||
|
studio.scene_count ||
|
||||||
studio.image_count ||
|
studio.image_count ||
|
||||||
studio.gallery_count ||
|
studio.gallery_count ||
|
||||||
studio.movie_count
|
studio.movie_count ||
|
||||||
|
studio.performer_count
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -131,6 +145,7 @@ export const StudioCard: React.FC<IProps> = ({
|
||||||
{maybeRenderMoviesPopoverButton()}
|
{maybeRenderMoviesPopoverButton()}
|
||||||
{maybeRenderImagesPopoverButton()}
|
{maybeRenderImagesPopoverButton()}
|
||||||
{maybeRenderGalleriesPopoverButton()}
|
{maybeRenderGalleriesPopoverButton()}
|
||||||
|
{maybeRenderPerformersPopoverButton()}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,15 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
eventKey="performers"
|
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} />
|
<StudioPerformersPanel studio={studio} />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,18 @@ const makeStudioMoviesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||||
return `/movies?${filter.makeQueryParameters()}`;
|
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>) => {
|
const makeChildStudiosUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||||
if (!studio.id) return "#";
|
if (!studio.id) return "#";
|
||||||
const filter = new ListFilterModel(GQL.FilterMode.Studios, undefined);
|
const filter = new ListFilterModel(GQL.FilterMode.Studios, undefined);
|
||||||
|
|
@ -307,6 +319,7 @@ export default {
|
||||||
makeStudioImagesUrl,
|
makeStudioImagesUrl,
|
||||||
makeStudioGalleriesUrl,
|
makeStudioGalleriesUrl,
|
||||||
makeStudioMoviesUrl,
|
makeStudioMoviesUrl,
|
||||||
|
makeStudioPerformersUrl,
|
||||||
makeParentTagsUrl,
|
makeParentTagsUrl,
|
||||||
makeChildTagsUrl,
|
makeChildTagsUrl,
|
||||||
makeTagSceneMarkersUrl,
|
makeTagSceneMarkersUrl,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue