From 9200f167bf0591e62480756eaed9d84f9487bcac Mon Sep 17 00:00:00 2001 From: peolic <66393006+peolic@users.noreply.github.com> Date: Tue, 20 Apr 2021 09:48:36 +0300 Subject: [PATCH] Add studio `*_count` filters and sort options (#1307) --- graphql/schema/types/filters.graphql | 6 + pkg/sqlite/image_test.go | 2 +- pkg/sqlite/setup_test.go | 55 ++++++--- pkg/sqlite/studio.go | 16 ++- pkg/sqlite/studio_test.go | 141 +++++++++++++++++++++++ ui/v2.5/src/models/list-filter/filter.ts | 39 ++++++- 6 files changed, 237 insertions(+), 22 deletions(-) diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index 6152174eb..8c1da2506 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -149,6 +149,12 @@ input StudioFilterType { stash_id: String """Filter to only include studios missing this property""" is_missing: String + """Filter by scene count""" + scene_count: IntCriterionInput + """Filter by image count""" + image_count: IntCriterionInput + """Filter by gallery count""" + gallery_count: IntCriterionInput """Filter by url""" url: StringCriterionInput } diff --git a/pkg/sqlite/image_test.go b/pkg/sqlite/image_test.go index d7260e477..50c3f35fc 100644 --- a/pkg/sqlite/image_test.go +++ b/pkg/sqlite/image_test.go @@ -126,7 +126,7 @@ func TestImageQueryPath(t *testing.T) { verifyImagePath(t, pathCriterion, totalImages-1) pathCriterion.Modifier = models.CriterionModifierMatchesRegex - pathCriterion.Value = "image_.*1_Path" + pathCriterion.Value = "image_.*01_Path" verifyImagePath(t, pathCriterion, 1) // TODO - 2 if zip path is included pathCriterion.Modifier = models.CriterionModifierNotMatchesRegex diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index f8a630095..481ebc629 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -34,6 +34,8 @@ const ( sceneIdxWithTag sceneIdxWithTwoTags sceneIdxWithStudio + sceneIdx1WithStudio + sceneIdx2WithStudio sceneIdxWithMarker sceneIdxWithPerformerTag sceneIdxWithPerformerTwoTags @@ -53,6 +55,8 @@ const ( imageIdxWithTag imageIdxWithTwoTags imageIdxWithStudio + imageIdx1WithStudio + imageIdx2WithStudio imageIdxInZip // TODO - not implemented imageIdxWithPerformerTag imageIdxWithPerformerTwoTags @@ -105,6 +109,8 @@ const ( galleryIdxWithTag galleryIdxWithTwoTags galleryIdxWithStudio + galleryIdx1WithStudio + galleryIdx2WithStudio galleryIdxWithPerformerTag galleryIdxWithPerformerTwoTags // new indexes above @@ -140,11 +146,14 @@ const ( const ( studioIdxWithScene = iota + studioIdxWithTwoScenes studioIdxWithMovie studioIdxWithChildStudio studioIdxWithParentStudio studioIdxWithImage + studioIdxWithTwoImages studioIdxWithGallery + studioIdxWithTwoGalleries // new indexes above // studios with dup names start from the end studioIdxWithDupName @@ -213,6 +222,8 @@ var ( sceneStudioLinks = [][2]int{ {sceneIdxWithStudio, studioIdxWithScene}, + {sceneIdx1WithStudio, studioIdxWithTwoScenes}, + {sceneIdx2WithStudio, studioIdxWithTwoScenes}, } ) @@ -222,6 +233,8 @@ var ( } imageStudioLinks = [][2]int{ {imageIdxWithStudio, studioIdxWithImage}, + {imageIdx1WithStudio, studioIdxWithTwoImages}, + {imageIdx2WithStudio, studioIdxWithTwoImages}, } imageTagLinks = [][2]int{ {imageIdxWithTag, tagIdxWithImage}, @@ -250,6 +263,12 @@ var ( {galleryIdx2WithPerformer, performerIdxWithTwoGalleries}, } + galleryStudioLinks = [][2]int{ + {galleryIdxWithStudio, studioIdxWithGallery}, + {galleryIdx1WithStudio, studioIdxWithTwoGalleries}, + {galleryIdx2WithStudio, studioIdxWithTwoGalleries}, + } + galleryTagLinks = [][2]int{ {galleryIdxWithTag, tagIdxWithGallery}, {galleryIdxWithTwoTags, tagIdx1WithGallery}, @@ -413,8 +432,8 @@ func populateDB() error { return fmt.Errorf("error linking gallery tags: %s", err.Error()) } - if err := linkGalleryStudio(r.Gallery(), galleryIdxWithStudio, studioIdxWithGallery); err != nil { - return fmt.Errorf("error linking gallery studio: %s", err.Error()) + if err := linkGalleryStudios(r.Gallery()); err != nil { + return fmt.Errorf("error linking gallery studios: %s", err.Error()) } if err := createMarker(r.SceneMarker(), sceneIdxWithMarker, tagIdxWithPrimaryMarker, []int{tagIdxWithMarker}); err != nil { @@ -1017,7 +1036,7 @@ func linkImagePerformers(qb models.ImageReaderWriter) error { func linkGalleryPerformers(qb models.GalleryReaderWriter) error { return doLinks(galleryPerformerLinks, func(galleryIndex, performerIndex int) error { - galleryID := imageIDs[galleryIndex] + galleryID := galleryIDs[galleryIndex] performers, err := qb.GetPerformerIDs(galleryID) if err != nil { return err @@ -1029,17 +1048,29 @@ func linkGalleryPerformers(qb models.GalleryReaderWriter) error { }) } -func linkGalleryTags(iqb models.GalleryReaderWriter) error { +func linkGalleryStudios(qb models.GalleryReaderWriter) error { + return doLinks(galleryStudioLinks, func(galleryIndex, studioIndex int) error { + gallery := models.GalleryPartial{ + ID: galleryIDs[galleryIndex], + StudioID: &sql.NullInt64{Int64: int64(studioIDs[studioIndex]), Valid: true}, + } + _, err := qb.UpdatePartial(gallery) + + return err + }) +} + +func linkGalleryTags(qb models.GalleryReaderWriter) error { return doLinks(galleryTagLinks, func(galleryIndex, tagIndex int) error { - galleryID := imageIDs[galleryIndex] - tags, err := iqb.GetTagIDs(galleryID) + galleryID := galleryIDs[galleryIndex] + tags, err := qb.GetTagIDs(galleryID) if err != nil { return err } tags = append(tags, tagIDs[tagIndex]) - return iqb.UpdateTags(galleryID, tags) + return qb.UpdateTags(galleryID, tags) }) } @@ -1070,13 +1101,3 @@ func linkStudiosParent(qb models.StudioWriter) error { func addTagImage(qb models.TagWriter, tagIndex int) error { return qb.UpdateImage(tagIDs[tagIndex], models.DefaultTagImage) } - -func linkGalleryStudio(qb models.GalleryWriter, galleryIndex, studioIndex int) error { - gallery := models.GalleryPartial{ - ID: galleryIDs[galleryIndex], - StudioID: &sql.NullInt64{Int64: int64(studioIDs[studioIndex]), Valid: true}, - } - _, err := qb.UpdatePartial(gallery) - - return err -} diff --git a/pkg/sqlite/studio.go b/pkg/sqlite/studio.go index c3a984c03..cbfb57f4d 100644 --- a/pkg/sqlite/studio.go +++ b/pkg/sqlite/studio.go @@ -133,7 +133,7 @@ func (qb *studioQueryBuilder) Query(studioFilter *models.StudioFilterType, findF query.body = selectDistinctIDs("studios") query.body += ` - left join scenes on studios.id = scenes.studio_id + left join scenes on studios.id = scenes.studio_id left join studio_stash_ids on studio_stash_ids.studio_id = studios.id ` @@ -165,6 +165,10 @@ func (qb *studioQueryBuilder) Query(studioFilter *models.StudioFilterType, findF query.addArg(stashIDFilter) } + query.handleCountCriterion(studioFilter.SceneCount, studioTable, sceneTable, studioIDColumn) + query.handleCountCriterion(studioFilter.ImageCount, studioTable, imageTable, studioIDColumn) + query.handleCountCriterion(studioFilter.GalleryCount, studioTable, galleryTable, studioIDColumn) + query.handleStringCriterionInput(studioFilter.URL, "studios.url") if isMissingFilter := studioFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" { @@ -209,7 +213,15 @@ func (qb *studioQueryBuilder) getStudioSort(findFilter *models.FindFilterType) s sort = findFilter.GetSort("name") direction = findFilter.GetDirection() } - return getSort(sort, direction, "studios") + + switch sort { + case "images_count": + return getCountSort(studioTable, imageTable, studioIDColumn, direction) + case "galleries_count": + return getCountSort(studioTable, galleryTable, studioIDColumn, direction) + default: + return getSort(sort, direction, "studios") + } } func (qb *studioQueryBuilder) queryStudio(query string, args []interface{}) (*models.Studio, error) { diff --git a/pkg/sqlite/studio_test.go b/pkg/sqlite/studio_test.go index 9325c720a..ae17c1cf7 100644 --- a/pkg/sqlite/studio_test.go +++ b/pkg/sqlite/studio_test.go @@ -265,6 +265,147 @@ func TestStudioDestroyStudioImage(t *testing.T) { } } +func TestStudioQuerySceneCount(t *testing.T) { + const sceneCount = 1 + sceneCountCriterion := models.IntCriterionInput{ + Value: sceneCount, + Modifier: models.CriterionModifierEquals, + } + + verifyStudiosSceneCount(t, sceneCountCriterion) + + sceneCountCriterion.Modifier = models.CriterionModifierNotEquals + verifyStudiosSceneCount(t, sceneCountCriterion) + + sceneCountCriterion.Modifier = models.CriterionModifierGreaterThan + verifyStudiosSceneCount(t, sceneCountCriterion) + + sceneCountCriterion.Modifier = models.CriterionModifierLessThan + verifyStudiosSceneCount(t, sceneCountCriterion) +} + +func verifyStudiosSceneCount(t *testing.T, sceneCountCriterion models.IntCriterionInput) { + withTxn(func(r models.Repository) error { + sqb := r.Studio() + studioFilter := models.StudioFilterType{ + SceneCount: &sceneCountCriterion, + } + + studios := queryStudio(t, sqb, &studioFilter, nil) + assert.Greater(t, len(studios), 0) + + for _, studio := range studios { + sceneCount, err := r.Scene().CountByStudioID(studio.ID) + if err != nil { + return err + } + verifyInt(t, sceneCount, sceneCountCriterion) + } + + return nil + }) +} + +func TestStudioQueryImageCount(t *testing.T) { + const imageCount = 1 + imageCountCriterion := models.IntCriterionInput{ + Value: imageCount, + Modifier: models.CriterionModifierEquals, + } + + verifyStudiosImageCount(t, imageCountCriterion) + + imageCountCriterion.Modifier = models.CriterionModifierNotEquals + verifyStudiosImageCount(t, imageCountCriterion) + + imageCountCriterion.Modifier = models.CriterionModifierGreaterThan + verifyStudiosImageCount(t, imageCountCriterion) + + imageCountCriterion.Modifier = models.CriterionModifierLessThan + verifyStudiosImageCount(t, imageCountCriterion) +} + +func verifyStudiosImageCount(t *testing.T, imageCountCriterion models.IntCriterionInput) { + withTxn(func(r models.Repository) error { + sqb := r.Studio() + studioFilter := models.StudioFilterType{ + ImageCount: &imageCountCriterion, + } + + studios := queryStudio(t, sqb, &studioFilter, nil) + assert.Greater(t, len(studios), 0) + + for _, studio := range studios { + pp := 0 + + _, count, err := r.Image().Query(&models.ImageFilterType{ + Studios: &models.MultiCriterionInput{ + Value: []string{strconv.Itoa(studio.ID)}, + Modifier: models.CriterionModifierIncludes, + }, + }, &models.FindFilterType{ + PerPage: &pp, + }) + if err != nil { + return err + } + verifyInt(t, count, imageCountCriterion) + } + + return nil + }) +} + +func TestStudioQueryGalleryCount(t *testing.T) { + const galleryCount = 1 + galleryCountCriterion := models.IntCriterionInput{ + Value: galleryCount, + Modifier: models.CriterionModifierEquals, + } + + verifyStudiosGalleryCount(t, galleryCountCriterion) + + galleryCountCriterion.Modifier = models.CriterionModifierNotEquals + verifyStudiosGalleryCount(t, galleryCountCriterion) + + galleryCountCriterion.Modifier = models.CriterionModifierGreaterThan + verifyStudiosGalleryCount(t, galleryCountCriterion) + + galleryCountCriterion.Modifier = models.CriterionModifierLessThan + verifyStudiosGalleryCount(t, galleryCountCriterion) +} + +func verifyStudiosGalleryCount(t *testing.T, galleryCountCriterion models.IntCriterionInput) { + withTxn(func(r models.Repository) error { + sqb := r.Studio() + studioFilter := models.StudioFilterType{ + GalleryCount: &galleryCountCriterion, + } + + studios := queryStudio(t, sqb, &studioFilter, nil) + assert.Greater(t, len(studios), 0) + + for _, studio := range studios { + pp := 0 + + _, count, err := r.Gallery().Query(&models.GalleryFilterType{ + Studios: &models.MultiCriterionInput{ + Value: []string{strconv.Itoa(studio.ID)}, + Modifier: models.CriterionModifierIncludes, + }, + }, &models.FindFilterType{ + PerPage: &pp, + }) + if err != nil { + return err + } + verifyInt(t, count, galleryCountCriterion) + } + + return nil + }) +} + func TestStudioStashIDs(t *testing.T) { if err := withTxn(func(r models.Repository) error { qb := r.Studio() diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index c17d2527f..2aad3672f 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -240,12 +240,21 @@ export class ListFilterModel { } case FilterMode.Studios: this.sortBy = "name"; - this.sortByOptions = ["name", "scenes_count", "random"]; + this.sortByOptions = [ + "name", + "scenes_count", + "images_count", + "galleries_count", + "random", + ]; this.displayModeOptions = [DisplayMode.Grid]; this.criterionOptions = [ new NoneCriterionOption(), new ParentStudiosCriterionOption(), new StudioIsMissingCriterionOption(), + ListFilterModel.createCriterionOption("scene_count"), + ListFilterModel.createCriterionOption("image_count"), + ListFilterModel.createCriterionOption("gallery_count"), ListFilterModel.createCriterionOption("url"), ]; break; @@ -1034,8 +1043,34 @@ export class ListFilterModel { }; break; } - case "studioIsMissing": + case "studioIsMissing": { result.is_missing = (criterion as IsMissingCriterion).value; + break; + } + case "scene_count": { + const countCrit = criterion as NumberCriterion; + result.scene_count = { + value: countCrit.value, + modifier: countCrit.modifier, + }; + break; + } + case "image_count": { + const countCrit = criterion as NumberCriterion; + result.image_count = { + value: countCrit.value, + modifier: countCrit.modifier, + }; + break; + } + case "gallery_count": { + const countCrit = criterion as NumberCriterion; + result.gallery_count = { + value: countCrit.value, + modifier: countCrit.modifier, + }; + break; + } // no default } });