diff --git a/graphql/schema/types/studio.graphql b/graphql/schema/types/studio.graphql index 097f04eb3..4c5778c5b 100644 --- a/graphql/schema/types/studio.graphql +++ b/graphql/schema/types/studio.graphql @@ -25,6 +25,7 @@ type Studio { updated_at: Time! groups: [Group!]! movies: [Movie!]! @deprecated(reason: "use groups instead") + o_counter: Int } input StudioCreateInput { diff --git a/internal/api/resolver_model_studio.go b/internal/api/resolver_model_studio.go index 850d42b54..fabcf38bd 100644 --- a/internal/api/resolver_model_studio.go +++ b/internal/api/resolver_model_studio.go @@ -143,6 +143,24 @@ func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, dep return r.GroupCount(ctx, obj, depth) } +func (r *studioResolver) OCounter(ctx context.Context, obj *models.Studio) (ret *int, err error) { + var res_scene int + var res_image int + var res int + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + res_scene, err = r.repository.Scene.OCountByStudioID(ctx, obj.ID) + if err != nil { + return err + } + res_image, err = r.repository.Image.OCountByStudioID(ctx, obj.ID) + return err + }); err != nil { + return nil, err + } + res = res_scene + res_image + return &res, nil +} + func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) { if obj.ParentID == nil { return nil, nil diff --git a/pkg/models/mocks/ImageReaderWriter.go b/pkg/models/mocks/ImageReaderWriter.go index 2bbf4ceeb..afc5efdb7 100644 --- a/pkg/models/mocks/ImageReaderWriter.go +++ b/pkg/models/mocks/ImageReaderWriter.go @@ -594,6 +594,27 @@ func (_m *ImageReaderWriter) OCountByPerformerID(ctx context.Context, performerI return r0, r1 } +// OCountByStudioID provides a mock function with given fields: ctx, studioID +func (_m *ImageReaderWriter) OCountByStudioID(ctx context.Context, studioID int) (int, error) { + ret := _m.Called(ctx, studioID) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context, int) int); ok { + r0 = rf(ctx, studioID) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, studioID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Query provides a mock function with given fields: ctx, options func (_m *ImageReaderWriter) Query(ctx context.Context, options models.ImageQueryOptions) (*models.ImageQueryResult, error) { ret := _m.Called(ctx, options) diff --git a/pkg/models/mocks/SceneReaderWriter.go b/pkg/models/mocks/SceneReaderWriter.go index bec31b6f2..ef10c890d 100644 --- a/pkg/models/mocks/SceneReaderWriter.go +++ b/pkg/models/mocks/SceneReaderWriter.go @@ -1183,6 +1183,27 @@ func (_m *SceneReaderWriter) OCountByPerformerID(ctx context.Context, performerI return r0, r1 } +// OCountByStudioID provides a mock function with given fields: ctx, studioID +func (_m *SceneReaderWriter) OCountByStudioID(ctx context.Context, studioID int) (int, error) { + ret := _m.Called(ctx, studioID) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context, int) int); ok { + r0 = rf(ctx, studioID) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, studioID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // PlayDuration provides a mock function with given fields: ctx func (_m *SceneReaderWriter) PlayDuration(ctx context.Context) (float64, error) { ret := _m.Called(ctx) diff --git a/pkg/models/repository_image.go b/pkg/models/repository_image.go index 1455d7762..672ecd063 100644 --- a/pkg/models/repository_image.go +++ b/pkg/models/repository_image.go @@ -38,6 +38,7 @@ type ImageCounter interface { CountByGalleryID(ctx context.Context, galleryID int) (int, error) OCount(ctx context.Context) (int, error) OCountByPerformerID(ctx context.Context, performerID int) (int, error) + OCountByStudioID(ctx context.Context, studioID int) (int, error) } // ImageCreator provides methods to create images. diff --git a/pkg/models/repository_scene.go b/pkg/models/repository_scene.go index fe0f473fb..8c2833470 100644 --- a/pkg/models/repository_scene.go +++ b/pkg/models/repository_scene.go @@ -45,6 +45,7 @@ type SceneCounter interface { CountMissingOSHash(ctx context.Context) (int, error) OCountByPerformerID(ctx context.Context, performerID int) (int, error) OCountByGroupID(ctx context.Context, groupID int) (int, error) + OCountByStudioID(ctx context.Context, studioID int) (int, error) } // SceneCreator provides methods to create scenes. diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index 6575ebb91..1588fa415 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -682,6 +682,20 @@ func (qb *ImageStore) OCountByPerformerID(ctx context.Context, performerID int) return ret, nil } +func (qb *ImageStore) OCountByStudioID(ctx context.Context, studioID int) (int, error) { + table := qb.table() + q := dialect.Select(goqu.COALESCE(goqu.SUM("o_counter"), 0)).From(table).Where( + table.Col(studioIDColumn).Eq(studioID), + ) + + var ret int + if err := querySimple(ctx, q, &ret); err != nil { + return 0, err + } + + return ret, nil +} + func (qb *ImageStore) OCount(ctx context.Context) (int, error) { table := qb.table() diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index 23f5ef482..40feb5847 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -818,6 +818,23 @@ func (qb *SceneStore) OCountByGroupID(ctx context.Context, groupID int) (int, er return ret, nil } +func (qb *SceneStore) OCountByStudioID(ctx context.Context, studioID int) (int, error) { + table := qb.table() + oHistoryTable := goqu.T(scenesODatesTable) + + q := dialect.Select(goqu.COUNT("*")).From(table).InnerJoin( + oHistoryTable, + goqu.On(table.Col(idColumn).Eq(oHistoryTable.Col(sceneIDColumn))), + ).Where(table.Col(studioIDColumn).Eq(studioID)) + + var ret int + if err := querySimple(ctx, q, &ret); err != nil { + return 0, err + } + + return ret, nil +} + func (qb *SceneStore) FindByGroupID(ctx context.Context, groupID int) ([]*models.Scene, error) { sq := dialect.From(scenesGroupsJoinTable).Select(scenesGroupsJoinTable.Col(sceneIDColumn)).Where( scenesGroupsJoinTable.Col(groupIDColumn).Eq(groupID), diff --git a/ui/v2.5/graphql/data/studio-slim.graphql b/ui/v2.5/graphql/data/studio-slim.graphql index cf101bd04..c48f7d93e 100644 --- a/ui/v2.5/graphql/data/studio-slim.graphql +++ b/ui/v2.5/graphql/data/studio-slim.graphql @@ -17,4 +17,5 @@ fragment SlimStudioData on Studio { id name } + o_counter } diff --git a/ui/v2.5/graphql/data/studio.graphql b/ui/v2.5/graphql/data/studio.graphql index d4ba79887..aabec7a9b 100644 --- a/ui/v2.5/graphql/data/studio.graphql +++ b/ui/v2.5/graphql/data/studio.graphql @@ -39,6 +39,7 @@ fragment StudioData on Studio { tags { ...SlimTagData } + o_counter } fragment SelectStudioData on Studio { diff --git a/ui/v2.5/src/components/Studios/StudioCard.tsx b/ui/v2.5/src/components/Studios/StudioCard.tsx index 5cd1cc209..01b2b5c5a 100644 --- a/ui/v2.5/src/components/Studios/StudioCard.tsx +++ b/ui/v2.5/src/components/Studios/StudioCard.tsx @@ -13,6 +13,7 @@ import { RatingBanner } from "../Shared/RatingBanner"; import { FavoriteIcon } from "../Shared/FavoriteIcon"; import { useStudioUpdate } from "src/core/StashService"; import { faTag } from "@fortawesome/free-solid-svg-icons"; +import { OCounterButton } from "../Shared/CountButton"; interface IProps { studio: GQL.StudioDataFragment; @@ -175,6 +176,12 @@ export const StudioCard: React.FC = ({ ); } + function maybeRenderOCounter() { + if (!studio.o_counter) return; + + return ; + } + function maybeRenderPopoverButtonGroup() { if ( studio.scene_count || @@ -182,6 +189,7 @@ export const StudioCard: React.FC = ({ studio.gallery_count || studio.group_count || studio.performer_count || + studio.o_counter || studio.tags.length > 0 ) { return ( @@ -194,6 +202,7 @@ export const StudioCard: React.FC = ({ {maybeRenderGalleriesPopoverButton()} {maybeRenderPerformersPopoverButton()} {maybeRenderTagPopoverButton()} + {maybeRenderOCounter()} ); diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx index c26ed0c73..2edc53fe1 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx @@ -48,6 +48,7 @@ import { ExternalLinkButtons } from "src/components/Shared/ExternalLinksButton"; import { AliasList } from "src/components/Shared/DetailsPage/AliasList"; import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage"; import { goBackOrReplace } from "src/utils/history"; +import { OCounterButton } from "src/components/Shared/CountButton"; interface IProps { studio: GQL.StudioDataFragment; @@ -471,12 +472,17 @@ const StudioPage: React.FC = ({ studio, tabKey }) => { - setRating(value)} - clickToRate - withoutContext - /> +
+ setRating(value)} + clickToRate + withoutContext + /> + {!!studio.o_counter && ( + + )} +
{!isEditing && (