mirror of
https://github.com/stashapp/stash.git
synced 2025-12-10 02:15:30 +01:00
Add movie count to performer and studio card (#1760)
* Add movies and movie_count properties to Performer type Extend the GraphQL API to allow getting the movies and movie count by performer. * Add movies count to performer card * Add movies and movie_count properties to Studio type Extend the GraphQL API to allow getting the movies and movie count by studio. * Add movies count to studio card
This commit is contained in:
parent
62af723017
commit
be94e52f21
15 changed files with 257 additions and 3 deletions
|
|
@ -22,6 +22,7 @@ fragment PerformerData on Performer {
|
|||
scene_count
|
||||
image_count
|
||||
gallery_count
|
||||
movie_count
|
||||
|
||||
tags {
|
||||
...SlimTagData
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ fragment StudioData on Studio {
|
|||
scene_count
|
||||
image_count
|
||||
gallery_count
|
||||
movie_count
|
||||
stash_ids {
|
||||
stash_id
|
||||
endpoint
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ type Performer {
|
|||
weight: Int
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
movie_count: Int
|
||||
movies: [Movie!]!
|
||||
}
|
||||
|
||||
input PerformerCreateInput {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ type Studio {
|
|||
details: String
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
movie_count: Int
|
||||
movies: [Movie!]!
|
||||
}
|
||||
|
||||
input StudioCreateInput {
|
||||
|
|
|
|||
|
|
@ -254,3 +254,26 @@ func (r *performerResolver) CreatedAt(ctx context.Context, obj *models.Performer
|
|||
func (r *performerResolver) UpdatedAt(ctx context.Context, obj *models.Performer) (*time.Time, error) {
|
||||
return &obj.UpdatedAt.Timestamp, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Movie, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Movie().FindByPerformerID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = repo.Movie().CountByPerformerID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,3 +151,26 @@ func (r *studioResolver) CreatedAt(ctx context.Context, obj *models.Studio) (*ti
|
|||
func (r *studioResolver) UpdatedAt(ctx context.Context, obj *models.Studio) (*time.Time, error) {
|
||||
return &obj.UpdatedAt.Timestamp, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Movie, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Movie().FindByStudioID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = repo.Movie().CountByStudioID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,48 @@ func (_m *MovieReaderWriter) Count() (int, error) {
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
// CountByPerformerID provides a mock function with given fields: performerID
|
||||
func (_m *MovieReaderWriter) CountByPerformerID(performerID int) (int, error) {
|
||||
ret := _m.Called(performerID)
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func(int) int); ok {
|
||||
r0 = rf(performerID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(int) error); ok {
|
||||
r1 = rf(performerID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CountByStudioID provides a mock function with given fields: studioID
|
||||
func (_m *MovieReaderWriter) CountByStudioID(studioID int) (int, error) {
|
||||
ret := _m.Called(studioID)
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func(int) int); ok {
|
||||
r0 = rf(studioID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(int) error); ok {
|
||||
r1 = rf(studioID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: newMovie
|
||||
func (_m *MovieReaderWriter) Create(newMovie models.Movie) (*models.Movie, error) {
|
||||
ret := _m.Called(newMovie)
|
||||
|
|
@ -176,6 +218,52 @@ func (_m *MovieReaderWriter) FindByNames(names []string, nocase bool) ([]*models
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
// FindByPerformerID provides a mock function with given fields: performerID
|
||||
func (_m *MovieReaderWriter) FindByPerformerID(performerID int) ([]*models.Movie, error) {
|
||||
ret := _m.Called(performerID)
|
||||
|
||||
var r0 []*models.Movie
|
||||
if rf, ok := ret.Get(0).(func(int) []*models.Movie); ok {
|
||||
r0 = rf(performerID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*models.Movie)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(int) error); ok {
|
||||
r1 = rf(performerID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// FindByStudioID provides a mock function with given fields: studioID
|
||||
func (_m *MovieReaderWriter) FindByStudioID(studioID int) ([]*models.Movie, error) {
|
||||
ret := _m.Called(studioID)
|
||||
|
||||
var r0 []*models.Movie
|
||||
if rf, ok := ret.Get(0).(func(int) []*models.Movie); ok {
|
||||
r0 = rf(studioID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*models.Movie)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(int) error); ok {
|
||||
r1 = rf(studioID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// FindMany provides a mock function with given fields: ids
|
||||
func (_m *MovieReaderWriter) FindMany(ids []int) ([]*models.Movie, error) {
|
||||
ret := _m.Called(ids)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ type MovieReader interface {
|
|||
Query(movieFilter *MovieFilterType, findFilter *FindFilterType) ([]*Movie, int, error)
|
||||
GetFrontImage(movieID int) ([]byte, error)
|
||||
GetBackImage(movieID int) ([]byte, error)
|
||||
FindByPerformerID(performerID int) ([]*Movie, error)
|
||||
CountByPerformerID(performerID int) (int, error)
|
||||
FindByStudioID(studioID int) ([]*Movie, error)
|
||||
CountByStudioID(studioID int) (int, error)
|
||||
}
|
||||
|
||||
type MovieWriter interface {
|
||||
|
|
|
|||
|
|
@ -308,3 +308,42 @@ func (qb *movieQueryBuilder) GetBackImage(movieID int) ([]byte, error) {
|
|||
query := `SELECT back_image from movies_images WHERE movie_id = ?`
|
||||
return getImage(qb.tx, query, movieID)
|
||||
}
|
||||
|
||||
func (qb *movieQueryBuilder) FindByPerformerID(performerID int) ([]*models.Movie, error) {
|
||||
query := `SELECT DISTINCT movies.*
|
||||
FROM movies
|
||||
INNER JOIN movies_scenes ON movies.id = movies_scenes.movie_id
|
||||
INNER JOIN performers_scenes ON performers_scenes.scene_id = movies_scenes.scene_id
|
||||
WHERE performers_scenes.performer_id = ?
|
||||
`
|
||||
args := []interface{}{performerID}
|
||||
return qb.queryMovies(query, args)
|
||||
}
|
||||
|
||||
func (qb *movieQueryBuilder) CountByPerformerID(performerID int) (int, error) {
|
||||
query := `SELECT COUNT(DISTINCT movies_scenes.movie_id) AS count
|
||||
FROM movies_scenes
|
||||
INNER JOIN performers_scenes ON performers_scenes.scene_id = movies_scenes.scene_id
|
||||
WHERE performers_scenes.performer_id = ?
|
||||
`
|
||||
args := []interface{}{performerID}
|
||||
return qb.runCountQuery(query, args)
|
||||
}
|
||||
|
||||
func (qb *movieQueryBuilder) FindByStudioID(studioID int) ([]*models.Movie, error) {
|
||||
query := `SELECT movies.*
|
||||
FROM movies
|
||||
WHERE movies.studio_id = ?
|
||||
`
|
||||
args := []interface{}{studioID}
|
||||
return qb.queryMovies(query, args)
|
||||
}
|
||||
|
||||
func (qb *movieQueryBuilder) CountByStudioID(studioID int) (int, error) {
|
||||
query := `SELECT COUNT(1) AS count
|
||||
FROM movies
|
||||
WHERE movies.studio_id = ?
|
||||
`
|
||||
args := []interface{}{studioID}
|
||||
return qb.runCountQuery(query, args)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
* Support filtering Movies by Performers. ([#1675](https://github.com/stashapp/stash/pull/1675))
|
||||
|
||||
### 🎨 Improvements
|
||||
* Added movie count to performer and studio cards. ([#1760](https://github.com/stashapp/stash/pull/1760))
|
||||
* Added date and details to Movie card, and move scene count to icon. ([#1758](https://github.com/stashapp/stash/pull/1758))
|
||||
* Added date and details to Gallery card, and move image count to icon. ([#1763](https://github.com/stashapp/stash/pull/1763))
|
||||
* Optimised image thumbnail generation (optionally using `libvips`) and made optional. ([#1655](https://github.com/stashapp/stash/pull/1655))
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export interface IPerformerCardExtraCriteria {
|
|||
scenes: Criterion<CriterionValue>[];
|
||||
images: Criterion<CriterionValue>[];
|
||||
galleries: Criterion<CriterionValue>[];
|
||||
movies: Criterion<CriterionValue>[];
|
||||
}
|
||||
|
||||
interface IPerformerCardProps {
|
||||
|
|
@ -124,18 +125,32 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
function maybeRenderMoviesPopoverButton() {
|
||||
if (!performer.movie_count) return;
|
||||
|
||||
return (
|
||||
<PopoverCountButton
|
||||
type="movie"
|
||||
count={performer.movie_count}
|
||||
url={NavUtils.makePerformerMoviesUrl(performer, extraCriteria?.movies)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderPopoverButtonGroup() {
|
||||
if (
|
||||
performer.scene_count ||
|
||||
performer.image_count ||
|
||||
performer.gallery_count ||
|
||||
performer.tags.length > 0
|
||||
performer.tags.length > 0 ||
|
||||
performer.movie_count
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<ButtonGroup className="card-popovers">
|
||||
{maybeRenderScenesPopoverButton()}
|
||||
{maybeRenderMoviesPopoverButton()}
|
||||
{maybeRenderImagesPopoverButton()}
|
||||
{maybeRenderGalleriesPopoverButton()}
|
||||
{maybeRenderTagPopoverButton()}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useIntl } from "react-intl";
|
|||
import { Link } from "react-router-dom";
|
||||
import Icon from "./Icon";
|
||||
|
||||
type PopoverLinkType = "scene" | "image" | "gallery";
|
||||
type PopoverLinkType = "scene" | "image" | "gallery" | "movie";
|
||||
|
||||
interface IProps {
|
||||
url: string;
|
||||
|
|
@ -23,6 +23,8 @@ export const PopoverCountButton: React.FC<IProps> = ({ url, type, count }) => {
|
|||
return "image";
|
||||
case "gallery":
|
||||
return "images";
|
||||
case "movie":
|
||||
return "film";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +45,11 @@ export const PopoverCountButton: React.FC<IProps> = ({ url, type, count }) => {
|
|||
one: "gallery",
|
||||
other: "galleries",
|
||||
};
|
||||
case "movie":
|
||||
return {
|
||||
one: "movie",
|
||||
other: "movies",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,13 +89,31 @@ export const StudioCard: React.FC<IProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
function maybeRenderMoviesPopoverButton() {
|
||||
if (!studio.movie_count) return;
|
||||
|
||||
return (
|
||||
<PopoverCountButton
|
||||
type="movie"
|
||||
count={studio.movie_count}
|
||||
url={NavUtils.makeStudioMoviesUrl(studio)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderPopoverButtonGroup() {
|
||||
if (studio.scene_count || studio.image_count || studio.gallery_count) {
|
||||
if (
|
||||
studio.scene_count ||
|
||||
studio.image_count ||
|
||||
studio.gallery_count ||
|
||||
studio.movie_count
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<ButtonGroup className="card-popovers">
|
||||
{maybeRenderScenesPopoverButton()}
|
||||
{maybeRenderMoviesPopoverButton()}
|
||||
{maybeRenderImagesPopoverButton()}
|
||||
{maybeRenderGalleriesPopoverButton()}
|
||||
</ButtonGroup>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export const StudioPerformersPanel: React.FC<IStudioPerformersPanel> = ({
|
|||
scenes: [studioCriterion],
|
||||
images: [studioCriterion],
|
||||
galleries: [studioCriterion],
|
||||
movies: [studioCriterion],
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -71,6 +71,21 @@ const makePerformerGalleriesUrl = (
|
|||
return `/galleries?${filter.makeQueryParameters()}`;
|
||||
};
|
||||
|
||||
const makePerformerMoviesUrl = (
|
||||
performer: Partial<GQL.PerformerDataFragment>,
|
||||
extraCriteria?: Criterion<CriterionValue>[]
|
||||
) => {
|
||||
if (!performer.id) return "#";
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Movies);
|
||||
const criterion = new PerformersCriterion();
|
||||
criterion.value = [
|
||||
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
|
||||
];
|
||||
filter.criteria.push(criterion);
|
||||
addExtraCriteria(filter.criteria, extraCriteria);
|
||||
return `/movies?${filter.makeQueryParameters()}`;
|
||||
};
|
||||
|
||||
const makePerformersCountryUrl = (
|
||||
performer: Partial<GQL.PerformerDataFragment>
|
||||
) => {
|
||||
|
|
@ -118,6 +133,18 @@ const makeStudioGalleriesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
|||
return `/galleries?${filter.makeQueryParameters()}`;
|
||||
};
|
||||
|
||||
const makeStudioMoviesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||
if (!studio.id) return "#";
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Movies);
|
||||
const criterion = new StudiosCriterion();
|
||||
criterion.value = {
|
||||
items: [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }],
|
||||
depth: 0,
|
||||
};
|
||||
filter.criteria.push(criterion);
|
||||
return `/movies?${filter.makeQueryParameters()}`;
|
||||
};
|
||||
|
||||
const makeChildStudiosUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||
if (!studio.id) return "#";
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Studios);
|
||||
|
|
@ -226,10 +253,12 @@ export default {
|
|||
makePerformerScenesUrl,
|
||||
makePerformerImagesUrl,
|
||||
makePerformerGalleriesUrl,
|
||||
makePerformerMoviesUrl,
|
||||
makePerformersCountryUrl,
|
||||
makeStudioScenesUrl,
|
||||
makeStudioImagesUrl,
|
||||
makeStudioGalleriesUrl,
|
||||
makeStudioMoviesUrl,
|
||||
makeTagSceneMarkersUrl,
|
||||
makeTagScenesUrl,
|
||||
makeTagPerformersUrl,
|
||||
|
|
|
|||
Loading…
Reference in a new issue