mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Add studio *_count filters and sort options (#1307)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user