added an url filter option in scenes (#1266)

* added an url filter option in scenes
* added url filter on gallery, movies, performers and studios
* Add empty string filter to stringCriterionHandler
* Add unit tests

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
julien0221
2021-04-09 06:05:11 +01:00
committed by GitHub
parent 60af076fff
commit 25311247ed
18 changed files with 480 additions and 52 deletions

View File

@@ -63,6 +63,8 @@ input PerformerFilterType {
tags: MultiCriterionInput
"""Filter by StashID"""
stash_id: String
"""Filter by url"""
url: StringCriterionInput
}
input SceneMarkerFilterType {
@@ -109,6 +111,8 @@ input SceneFilterType {
performers: MultiCriterionInput
"""Filter by StashID"""
stash_id: String
"""Filter by url"""
url: StringCriterionInput
}
input MovieFilterType {
@@ -116,6 +120,8 @@ input MovieFilterType {
studios: MultiCriterionInput
"""Filter to only include movies missing this property"""
is_missing: String
"""Filter by url"""
url: StringCriterionInput
}
input StudioFilterType {
@@ -125,6 +131,8 @@ input StudioFilterType {
stash_id: String
"""Filter to only include studios missing this property"""
is_missing: String
"""Filter by url"""
url: StringCriterionInput
}
input GalleryFilterType {
@@ -150,6 +158,8 @@ input GalleryFilterType {
performers: MultiCriterionInput
"""Filter by number of images in this gallery"""
image_count: IntCriterionInput
"""Filter by url"""
url: StringCriterionInput
}
input TagFilterType {

View File

@@ -321,6 +321,10 @@ func stringCriterionHandler(c *models.StringCriterionInput, column string) crite
return
}
f.addWhere(fmt.Sprintf("(%s IS NULL OR %[1]s NOT regexp ?)", column), c.Value)
case models.CriterionModifierIsNull:
f.addWhere("(" + column + " IS NULL OR TRIM(" + column + ") = '')")
case models.CriterionModifierNotNull:
f.addWhere("(" + column + " IS NOT NULL AND TRIM(" + column + ") != '')")
default:
clause, count := getSimpleCriterionClause(modifier, "?")

View File

@@ -594,7 +594,7 @@ func TestStringCriterionHandlerIsNull(t *testing.T) {
}, column))
assert.Len(f.whereClauses, 1)
assert.Equal(fmt.Sprintf("%[1]s IS NULL", column), f.whereClauses[0].sql)
assert.Equal(fmt.Sprintf("(%[1]s IS NULL OR TRIM(%[1]s) = '')", column), f.whereClauses[0].sql)
assert.Len(f.whereClauses[0].args, 0)
}
@@ -609,6 +609,6 @@ func TestStringCriterionHandlerNotNull(t *testing.T) {
}, column))
assert.Len(f.whereClauses, 1)
assert.Equal(fmt.Sprintf("%[1]s IS NOT NULL", column), f.whereClauses[0].sql)
assert.Equal(fmt.Sprintf("(%[1]s IS NOT NULL AND TRIM(%[1]s) != '')", column), f.whereClauses[0].sql)
assert.Len(f.whereClauses[0].args, 0)
}

View File

@@ -198,6 +198,7 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi
query.handleStringCriterionInput(galleryFilter.Path, "galleries.path")
query.handleIntCriterionInput(galleryFilter.Rating, "galleries.rating")
query.handleStringCriterionInput(galleryFilter.URL, "galleries.url")
qb.handleAverageResolutionFilter(&query, galleryFilter.AverageResolution)
if Organized := galleryFilter.Organized; Organized != nil {

View File

@@ -193,6 +193,62 @@ func verifyGalleriesPath(t *testing.T, sqb models.GalleryReader, pathCriterion m
}
}
func TestGalleryQueryURL(t *testing.T) {
const sceneIdx = 1
galleryURL := getGalleryStringValue(sceneIdx, urlField)
urlCriterion := models.StringCriterionInput{
Value: galleryURL,
Modifier: models.CriterionModifierEquals,
}
filter := models.GalleryFilterType{
URL: &urlCriterion,
}
verifyFn := func(g *models.Gallery) {
t.Helper()
verifyNullString(t, g.URL, urlCriterion)
}
verifyGalleryQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotEquals
verifyGalleryQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierMatchesRegex
urlCriterion.Value = "gallery_.*1_URL"
verifyGalleryQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotMatchesRegex
verifyGalleryQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierIsNull
urlCriterion.Value = ""
verifyGalleryQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotNull
verifyGalleryQuery(t, filter, verifyFn)
}
func verifyGalleryQuery(t *testing.T, filter models.GalleryFilterType, verifyFn func(s *models.Gallery)) {
withTxn(func(r models.Repository) error {
t.Helper()
sqb := r.Gallery()
galleries := queryGallery(t, sqb, &filter, nil)
// assume it should find at least one
assert.Greater(t, len(galleries), 0)
for _, gallery := range galleries {
verifyFn(gallery)
}
return nil
})
}
func TestGalleryQueryRating(t *testing.T) {
const rating = 3
ratingCriterion := models.IntCriterionInput{

View File

@@ -122,11 +122,10 @@ func (qb *movieQueryBuilder) Query(movieFilter *models.MovieFilterType, findFilt
movieFilter = &models.MovieFilterType{}
}
var whereClauses []string
var havingClauses []string
var args []interface{}
body := selectDistinctIDs("movies")
body += `
query := qb.newQuery()
query.body = selectDistinctIDs("movies")
query.body += `
left join movies_scenes as scenes_join on scenes_join.movie_id = movies.id
left join scenes on scenes_join.scene_id = scenes.id
left join studios as studio on studio.id = movies.studio_id
@@ -135,41 +134,43 @@ func (qb *movieQueryBuilder) Query(movieFilter *models.MovieFilterType, findFilt
if q := findFilter.Q; q != nil && *q != "" {
searchColumns := []string{"movies.name"}
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
whereClauses = append(whereClauses, clause)
args = append(args, thisArgs...)
query.addWhere(clause)
query.addArg(thisArgs...)
}
if studiosFilter := movieFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 {
for _, studioID := range studiosFilter.Value {
args = append(args, studioID)
query.addArg(studioID)
}
whereClause, havingClause := getMultiCriterionClause("movies", "studio", "", "", "studio_id", studiosFilter)
whereClauses = appendClause(whereClauses, whereClause)
havingClauses = appendClause(havingClauses, havingClause)
query.addWhere(whereClause)
query.addHaving(havingClause)
}
if isMissingFilter := movieFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
switch *isMissingFilter {
case "front_image":
body += `left join movies_images on movies_images.movie_id = movies.id
query.body += `left join movies_images on movies_images.movie_id = movies.id
`
whereClauses = appendClause(whereClauses, "movies_images.front_image IS NULL")
query.addWhere("movies_images.front_image IS NULL")
case "back_image":
body += `left join movies_images on movies_images.movie_id = movies.id
query.body += `left join movies_images on movies_images.movie_id = movies.id
`
whereClauses = appendClause(whereClauses, "movies_images.back_image IS NULL")
query.addWhere("movies_images.back_image IS NULL")
case "scenes":
body += `left join movies_scenes on movies_scenes.movie_id = movies.id
query.body += `left join movies_scenes on movies_scenes.movie_id = movies.id
`
whereClauses = appendClause(whereClauses, "movies_scenes.scene_id IS NULL")
query.addWhere("movies_scenes.scene_id IS NULL")
default:
whereClauses = appendClause(whereClauses, "movies."+*isMissingFilter+" IS NULL")
query.addWhere("movies." + *isMissingFilter + " IS NULL")
}
}
sortAndPagination := qb.getMovieSort(findFilter) + getPagination(findFilter)
idsResult, countResult, err := qb.executeFindQuery(body, args, sortAndPagination, whereClauses, havingClauses)
query.handleStringCriterionInput(movieFilter.URL, "movies.url")
query.sortAndPagination = qb.getMovieSort(findFilter) + getPagination(findFilter)
idsResult, countResult, err := query.executeFind()
if err != nil {
return nil, 0, err
}

View File

@@ -119,6 +119,71 @@ func TestMovieQueryStudio(t *testing.T) {
})
}
func TestMovieQueryURL(t *testing.T) {
const sceneIdx = 1
movieURL := getMovieStringValue(sceneIdx, urlField)
urlCriterion := models.StringCriterionInput{
Value: movieURL,
Modifier: models.CriterionModifierEquals,
}
filter := models.MovieFilterType{
URL: &urlCriterion,
}
verifyFn := func(n *models.Movie) {
t.Helper()
verifyNullString(t, n.URL, urlCriterion)
}
verifyMovieQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotEquals
verifyMovieQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierMatchesRegex
urlCriterion.Value = "movie_.*1_URL"
verifyMovieQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotMatchesRegex
verifyMovieQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierIsNull
urlCriterion.Value = ""
verifyMovieQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotNull
verifyMovieQuery(t, filter, verifyFn)
}
func verifyMovieQuery(t *testing.T, filter models.MovieFilterType, verifyFn func(s *models.Movie)) {
withTxn(func(r models.Repository) error {
t.Helper()
sqb := r.Movie()
movies := queryMovie(t, sqb, &filter, nil)
// assume it should find at least one
assert.Greater(t, len(movies), 0)
for _, m := range movies {
verifyFn(m)
}
return nil
})
}
func queryMovie(t *testing.T, sqb models.MovieReader, movieFilter *models.MovieFilterType, findFilter *models.FindFilterType) []*models.Movie {
movies, _, err := sqb.Query(movieFilter, findFilter)
if err != nil {
t.Errorf("Error querying movie: %s", err.Error())
}
return movies
}
func TestMovieUpdateMovieImages(t *testing.T) {
if err := withTxn(func(r models.Repository) error {
mqb := r.Movie()

View File

@@ -254,6 +254,7 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy
query.handleStringCriterionInput(performerFilter.CareerLength, tableName+".career_length")
query.handleStringCriterionInput(performerFilter.Tattoos, tableName+".tattoos")
query.handleStringCriterionInput(performerFilter.Piercings, tableName+".piercings")
query.handleStringCriterionInput(performerFilter.URL, tableName+".url")
// TODO - need better handling of aliases
query.handleStringCriterionInput(performerFilter.Aliases, tableName+".aliases")

View File

@@ -268,6 +268,62 @@ func verifyPerformerCareerLength(t *testing.T, criterion models.StringCriterionI
})
}
func TestPerformerQueryURL(t *testing.T) {
const sceneIdx = 1
performerURL := getPerformerStringValue(sceneIdx, urlField)
urlCriterion := models.StringCriterionInput{
Value: performerURL,
Modifier: models.CriterionModifierEquals,
}
filter := models.PerformerFilterType{
URL: &urlCriterion,
}
verifyFn := func(g *models.Performer) {
t.Helper()
verifyNullString(t, g.URL, urlCriterion)
}
verifyPerformerQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotEquals
verifyPerformerQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierMatchesRegex
urlCriterion.Value = "performer_.*1_URL"
verifyPerformerQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotMatchesRegex
verifyPerformerQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierIsNull
urlCriterion.Value = ""
verifyPerformerQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotNull
verifyPerformerQuery(t, filter, verifyFn)
}
func verifyPerformerQuery(t *testing.T, filter models.PerformerFilterType, verifyFn func(s *models.Performer)) {
withTxn(func(r models.Repository) error {
t.Helper()
sqb := r.Performer()
performers := queryPerformers(t, sqb, &filter, nil)
// assume it should find at least one
assert.Greater(t, len(performers), 0)
for _, p := range performers {
verifyFn(p)
}
return nil
})
}
func queryPerformers(t *testing.T, qb models.PerformerReader, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) []*models.Performer {
performers, _, err := qb.Query(performerFilter, findFilter)
if err != nil {

View File

@@ -345,6 +345,7 @@ func (qb *sceneQueryBuilder) makeFilter(sceneFilter *models.SceneFilterType) *fi
query.handleCriterionFunc(resolutionCriterionHandler(sceneFilter.Resolution, "scenes.height", "scenes.width"))
query.handleCriterionFunc(hasMarkersCriterionHandler(sceneFilter.HasMarkers))
query.handleCriterionFunc(sceneIsMissingCriterionHandler(qb, sceneFilter.IsMissing))
query.handleCriterionFunc(stringCriterionHandler(sceneFilter.URL, "scenes.url"))
query.handleCriterionFunc(sceneTagsCriterionHandler(qb, sceneFilter.Tags))
query.handleCriterionFunc(scenePerformersCriterionHandler(qb, sceneFilter.Performers))

View File

@@ -187,6 +187,44 @@ func TestSceneQueryPath(t *testing.T) {
verifyScenesPath(t, pathCriterion)
}
func TestSceneQueryURL(t *testing.T) {
const sceneIdx = 1
scenePath := getSceneStringValue(sceneIdx, urlField)
urlCriterion := models.StringCriterionInput{
Value: scenePath,
Modifier: models.CriterionModifierEquals,
}
filter := models.SceneFilterType{
URL: &urlCriterion,
}
verifyFn := func(s *models.Scene) {
t.Helper()
verifyNullString(t, s.URL, urlCriterion)
}
verifySceneQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotEquals
verifySceneQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierMatchesRegex
urlCriterion.Value = "scene_.*1_URL"
verifySceneQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotMatchesRegex
verifySceneQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierIsNull
urlCriterion.Value = ""
verifySceneQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotNull
verifySceneQuery(t, filter, verifyFn)
}
func TestSceneQueryPathOr(t *testing.T) {
const scene1Idx = 1
const scene2Idx = 2
@@ -324,6 +362,24 @@ func TestSceneIllegalQuery(t *testing.T) {
})
}
func verifySceneQuery(t *testing.T, filter models.SceneFilterType, verifyFn func(s *models.Scene)) {
withTxn(func(r models.Repository) error {
t.Helper()
sqb := r.Scene()
scenes := queryScene(t, sqb, &filter, nil)
// assume it should find at least one
assert.Greater(t, len(scenes), 0)
for _, scene := range scenes {
verifyFn(scene)
}
return nil
})
}
func verifyScenesPath(t *testing.T, pathCriterion models.StringCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Scene()
@@ -345,10 +401,15 @@ func verifyNullString(t *testing.T, value sql.NullString, criterion models.Strin
t.Helper()
assert := assert.New(t)
if criterion.Modifier == models.CriterionModifierIsNull {
if value.Valid && value.String == "" {
// correct
return
}
assert.False(value.Valid, "expect is null values to be null")
}
if criterion.Modifier == models.CriterionModifierNotNull {
assert.True(value.Valid, "expect is null values to be null")
assert.Greater(len(value.String), 0)
}
if criterion.Modifier == models.CriterionModifierEquals {
assert.Equal(criterion.Value, value.String)

View File

@@ -76,7 +76,8 @@ const (
movieIdxWithScene = iota
movieIdxWithStudio
// movies with dup names start from the end
movieIdxWithDupName
// create 10 more basic movies (can remove this if we add more indexes)
movieIdxWithDupName = movieIdxWithStudio + 10
moviesNameCase = movieIdxWithDupName
moviesNameNoCase = 1
@@ -146,6 +147,7 @@ const (
pathField = "Path"
checksumField = "Checksum"
titleField = "Title"
urlField = "URL"
zipPath = "zipPath.zip"
)
@@ -407,8 +409,32 @@ func populateDB() error {
return nil
}
func getPrefixedStringValue(prefix string, index int, field string) string {
return fmt.Sprintf("%s_%04d_%s", prefix, index, field)
}
func getPrefixedNullStringValue(prefix string, index int, field string) sql.NullString {
if index > 0 && index%5 == 0 {
return sql.NullString{}
}
if index > 0 && index%6 == 0 {
return sql.NullString{
String: "",
Valid: true,
}
}
return sql.NullString{
String: getPrefixedStringValue(prefix, index, field),
Valid: true,
}
}
func getSceneStringValue(index int, field string) string {
return fmt.Sprintf("scene_%04d_%s", index, field)
return getPrefixedStringValue("scene", index, field)
}
func getSceneNullStringValue(index int, field string) sql.NullString {
return getPrefixedNullStringValue("scene", index, field)
}
func getRating(index int) sql.NullInt64 {
@@ -455,6 +481,7 @@ func createScenes(sqb models.SceneReaderWriter, n int) error {
Title: sql.NullString{String: getSceneStringValue(i, titleField), Valid: true},
Checksum: sql.NullString{String: getSceneStringValue(i, checksumField), Valid: true},
Details: sql.NullString{String: getSceneStringValue(i, "Details"), Valid: true},
URL: getSceneNullStringValue(i, urlField),
Rating: getRating(i),
OCounter: getOCounter(i),
Duration: getSceneDuration(i),
@@ -511,13 +538,18 @@ func createImages(qb models.ImageReaderWriter, n int) error {
}
func getGalleryStringValue(index int, field string) string {
return "gallery_" + strconv.FormatInt(int64(index), 10) + "_" + field
return getPrefixedStringValue("gallery", index, field)
}
func getGalleryNullStringValue(index int, field string) sql.NullString {
return getPrefixedNullStringValue("gallery", index, field)
}
func createGalleries(gqb models.GalleryReaderWriter, n int) error {
for i := 0; i < n; i++ {
gallery := models.Gallery{
Path: models.NullString(getGalleryStringValue(i, pathField)),
URL: getGalleryNullStringValue(i, urlField),
Checksum: getGalleryStringValue(i, checksumField),
}
@@ -534,10 +566,14 @@ func createGalleries(gqb models.GalleryReaderWriter, n int) error {
}
func getMovieStringValue(index int, field string) string {
return "movie_" + strconv.FormatInt(int64(index), 10) + "_" + field
return getPrefixedStringValue("movie", index, field)
}
//createMoviees creates n movies with plain Name and o movies with camel cased NaMe included
func getMovieNullStringValue(index int, field string) sql.NullString {
return getPrefixedNullStringValue("movie", index, field)
}
// createMoviees creates n movies with plain Name and o movies with camel cased NaMe included
func createMovies(mqb models.MovieReaderWriter, n int, o int) error {
const namePlain = "Name"
const nameNoCase = "NaMe"
@@ -555,6 +591,7 @@ func createMovies(mqb models.MovieReaderWriter, n int, o int) error {
name = getMovieStringValue(index, name)
movie := models.Movie{
Name: sql.NullString{String: name, Valid: true},
URL: getMovieNullStringValue(index, urlField),
Checksum: utils.MD5FromString(name),
}
@@ -572,7 +609,11 @@ func createMovies(mqb models.MovieReaderWriter, n int, o int) error {
}
func getPerformerStringValue(index int, field string) string {
return "performer_" + strconv.FormatInt(int64(index), 10) + "_" + field
return getPrefixedStringValue("performer", index, field)
}
func getPerformerNullStringValue(index int, field string) sql.NullString {
return getPrefixedNullStringValue("performer", index, field)
}
func getPerformerBoolValue(index int) bool {
@@ -596,7 +637,7 @@ func getPerformerCareerLength(index int) *string {
return &ret
}
//createPerformers creates n performers with plain Name and o performers with camel cased NaMe included
// createPerformers creates n performers with plain Name and o performers with camel cased NaMe included
func createPerformers(pqb models.PerformerReaderWriter, n int, o int) error {
const namePlain = "Name"
const nameNoCase = "NaMe"
@@ -615,6 +656,7 @@ func createPerformers(pqb models.PerformerReaderWriter, n int, o int) error {
performer := models.Performer{
Name: sql.NullString{String: getPerformerStringValue(index, name), Valid: true},
Checksum: getPerformerStringValue(i, checksumField),
URL: getPerformerNullStringValue(i, urlField),
Favorite: sql.NullBool{Bool: getPerformerBoolValue(i), Valid: true},
Birthdate: models.SQLiteDate{
String: getPerformerBirthdate(i),
@@ -718,7 +760,11 @@ func createTags(tqb models.TagReaderWriter, n int, o int) error {
}
func getStudioStringValue(index int, field string) string {
return "studio_" + strconv.FormatInt(int64(index), 10) + "_" + field
return getPrefixedStringValue("studio", index, field)
}
func getStudioNullStringValue(index int, field string) sql.NullString {
return getPrefixedNullStringValue("studio", index, field)
}
func createStudio(sqb models.StudioReaderWriter, name string, parentID *int64) (*models.Studio, error) {
@@ -731,6 +777,10 @@ func createStudio(sqb models.StudioReaderWriter, name string, parentID *int64) (
studio.ParentID = sql.NullInt64{Int64: *parentID, Valid: true}
}
return createStudioFromModel(sqb, studio)
}
func createStudioFromModel(sqb models.StudioReaderWriter, studio models.Studio) (*models.Studio, error) {
created, err := sqb.Create(studio)
if err != nil {
@@ -740,7 +790,7 @@ func createStudio(sqb models.StudioReaderWriter, name string, parentID *int64) (
return created, nil
}
//createStudios creates n studios with plain Name and o studios with camel cased NaMe included
// createStudios creates n studios with plain Name and o studios with camel cased NaMe included
func createStudios(sqb models.StudioReaderWriter, n int, o int) error {
const namePlain = "Name"
const nameNoCase = "NaMe"
@@ -756,7 +806,12 @@ func createStudios(sqb models.StudioReaderWriter, n int, o int) error {
// studios [ i ] and [ n + o - i - 1 ] should have similar names with only the Name!=NaMe part different
name = getStudioStringValue(index, name)
created, err := createStudio(sqb, name, nil)
studio := models.Studio{
Name: sql.NullString{String: name, Valid: true},
Checksum: utils.MD5FromString(name),
URL: getStudioNullStringValue(index, urlField),
}
created, err := createStudioFromModel(sqb, studio)
if err != nil {
return err

View File

@@ -129,11 +129,10 @@ func (qb *studioQueryBuilder) Query(studioFilter *models.StudioFilterType, findF
findFilter = &models.FindFilterType{}
}
var whereClauses []string
var havingClauses []string
var args []interface{}
body := selectDistinctIDs("studios")
body += `
query := qb.newQuery()
query.body = selectDistinctIDs("studios")
query.body += `
left join scenes on studios.id = scenes.studio_id
left join studio_stash_ids on studio_stash_ids.studio_id = studios.id
`
@@ -142,44 +141,47 @@ func (qb *studioQueryBuilder) Query(studioFilter *models.StudioFilterType, findF
searchColumns := []string{"studios.name"}
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
whereClauses = append(whereClauses, clause)
args = append(args, thisArgs...)
query.addWhere(clause)
query.addArg(thisArgs...)
}
if parentsFilter := studioFilter.Parents; parentsFilter != nil && len(parentsFilter.Value) > 0 {
body += `
query.body += `
left join studios as parent_studio on parent_studio.id = studios.parent_id
`
for _, studioID := range parentsFilter.Value {
args = append(args, studioID)
query.addArg(studioID)
}
whereClause, havingClause := getMultiCriterionClause("studios", "parent_studio", "", "", "parent_id", parentsFilter)
whereClauses = appendClause(whereClauses, whereClause)
havingClauses = appendClause(havingClauses, havingClause)
query.addWhere(whereClause)
query.addHaving(havingClause)
}
if stashIDFilter := studioFilter.StashID; stashIDFilter != nil {
whereClauses = append(whereClauses, "studio_stash_ids.stash_id = ?")
args = append(args, stashIDFilter)
query.addWhere("studio_stash_ids.stash_id = ?")
query.addArg(stashIDFilter)
}
query.handleStringCriterionInput(studioFilter.URL, "studios.url")
if isMissingFilter := studioFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
switch *isMissingFilter {
case "image":
body += `left join studios_image on studios_image.studio_id = studios.id
query.body += `left join studios_image on studios_image.studio_id = studios.id
`
whereClauses = appendClause(whereClauses, "studios_image.studio_id IS NULL")
query.addWhere("studios_image.studio_id IS NULL")
case "stash_id":
whereClauses = appendClause(whereClauses, "studio_stash_ids.studio_id IS NULL")
query.addWhere("studio_stash_ids.studio_id IS NULL")
default:
whereClauses = appendClause(whereClauses, "studios."+*isMissingFilter+" IS NULL")
query.addWhere("studios." + *isMissingFilter + " IS NULL")
}
}
sortAndPagination := qb.getStudioSort(findFilter) + getPagination(findFilter)
idsResult, countResult, err := qb.executeFindQuery(body, args, sortAndPagination, whereClauses, havingClauses)
query.sortAndPagination = qb.getStudioSort(findFilter) + getPagination(findFilter)
idsResult, countResult, err := query.executeFind()
if err != nil {
return nil, 0, err
}

View File

@@ -283,6 +283,71 @@ func TestStudioStashIDs(t *testing.T) {
}
}
func TestStudioQueryURL(t *testing.T) {
const sceneIdx = 1
studioURL := getStudioStringValue(sceneIdx, urlField)
urlCriterion := models.StringCriterionInput{
Value: studioURL,
Modifier: models.CriterionModifierEquals,
}
filter := models.StudioFilterType{
URL: &urlCriterion,
}
verifyFn := func(g *models.Studio) {
t.Helper()
verifyNullString(t, g.URL, urlCriterion)
}
verifyStudioQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotEquals
verifyStudioQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierMatchesRegex
urlCriterion.Value = "studio_.*1_URL"
verifyStudioQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotMatchesRegex
verifyStudioQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierIsNull
urlCriterion.Value = ""
verifyStudioQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotNull
verifyStudioQuery(t, filter, verifyFn)
}
func verifyStudioQuery(t *testing.T, filter models.StudioFilterType, verifyFn func(s *models.Studio)) {
withTxn(func(r models.Repository) error {
t.Helper()
sqb := r.Studio()
galleries := queryStudio(t, sqb, &filter, nil)
// assume it should find at least one
assert.Greater(t, len(galleries), 0)
for _, studio := range galleries {
verifyFn(studio)
}
return nil
})
}
func queryStudio(t *testing.T, sqb models.StudioReader, studioFilter *models.StudioFilterType, findFilter *models.FindFilterType) []*models.Studio {
studios, _, err := sqb.Query(studioFilter, findFilter)
if err != nil {
t.Errorf("Error querying studio: %s", err.Error())
}
return studios
}
// TODO Create
// TODO Update
// TODO Destroy

View File

@@ -3,7 +3,8 @@
* Added scene queue.
### 🎨 Improvements
* Add HTTP endpoint for health checking at /healthz.
* Add URL filter criteria for scenes, galleries, movies, performers and studios.
* Add HTTP endpoint for health checking at `/healthz`.
* Support `today` and `yesterday` for `parseDate` in scrapers.
* Add random sorting option for galleries, studios, movies and tags.
* Disable sounds on scene/marker wall previews by default.

View File

@@ -47,7 +47,8 @@ export type CriterionType =
| "marker_count"
| "image_count"
| "gallery_count"
| "performer_count";
| "performer_count"
| "url";
type Option = string | number | IOptionType;
export type CriterionValue = string | number | ILabeledId[];
@@ -135,6 +136,8 @@ export abstract class Criterion {
return "Gallery Count";
case "performer_count":
return "Performer Count";
case "url":
return "URL";
}
}

View File

@@ -112,6 +112,7 @@ export function makeCriteria(type: CriterionType = "none") {
case "tattoos":
case "piercings":
case "aliases":
case "url":
return new StringCriterion(type, type);
}
}

View File

@@ -151,6 +151,7 @@ export class ListFilterModel {
new PerformersCriterionOption(),
new StudiosCriterionOption(),
new MoviesCriterionOption(),
ListFilterModel.createCriterionOption("url"),
];
break;
case FilterMode.Images:
@@ -210,6 +211,7 @@ export class ListFilterModel {
new GenderCriterionOption(),
new PerformerIsMissingCriterionOption(),
new TagsCriterionOption(),
ListFilterModel.createCriterionOption("url"),
...numberCriteria
.concat(stringCriteria)
.map((c) => ListFilterModel.createCriterionOption(c)),
@@ -225,6 +227,7 @@ export class ListFilterModel {
new NoneCriterionOption(),
new ParentStudiosCriterionOption(),
new StudioIsMissingCriterionOption(),
ListFilterModel.createCriterionOption("url"),
];
break;
case FilterMode.Movies:
@@ -235,6 +238,7 @@ export class ListFilterModel {
new NoneCriterionOption(),
new StudiosCriterionOption(),
new MovieIsMissingCriterionOption(),
ListFilterModel.createCriterionOption("url"),
];
break;
case FilterMode.Galleries:
@@ -257,6 +261,7 @@ export class ListFilterModel {
new PerformerTagsCriterionOption(),
new PerformersCriterionOption(),
new StudiosCriterionOption(),
ListFilterModel.createCriterionOption("url"),
];
this.displayModeOptions = [
DisplayMode.Grid,
@@ -582,6 +587,14 @@ export class ListFilterModel {
};
break;
}
case "url": {
const urlCrit = criterion as StringCriterion;
result.url = {
value: urlCrit.value,
modifier: urlCrit.modifier,
};
break;
}
// no default
}
});
@@ -690,6 +703,14 @@ export class ListFilterModel {
};
break;
}
case "url": {
const urlCrit = criterion as StringCriterion;
result.url = {
value: urlCrit.value,
modifier: urlCrit.modifier,
};
break;
}
// no default
}
});
@@ -868,6 +889,14 @@ export class ListFilterModel {
};
break;
}
case "url": {
const urlCrit = criterion as StringCriterion;
result.url = {
value: urlCrit.value,
modifier: urlCrit.modifier,
};
break;
}
case "movieIsMissing":
result.is_missing = (criterion as IsMissingCriterion).value;
// no default
@@ -888,6 +917,14 @@ export class ListFilterModel {
};
break;
}
case "url": {
const urlCrit = criterion as StringCriterion;
result.url = {
value: urlCrit.value,
modifier: urlCrit.modifier,
};
break;
}
case "studioIsMissing":
result.is_missing = (criterion as IsMissingCriterion).value;
// no default
@@ -1001,6 +1038,14 @@ export class ListFilterModel {
};
break;
}
case "url": {
const urlCrit = criterion as StringCriterion;
result.url = {
value: urlCrit.value,
modifier: urlCrit.modifier,
};
break;
}
// no default
}
});