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

View File

@@ -321,6 +321,10 @@ func stringCriterionHandler(c *models.StringCriterionInput, column string) crite
return return
} }
f.addWhere(fmt.Sprintf("(%s IS NULL OR %[1]s NOT regexp ?)", column), c.Value) 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: default:
clause, count := getSimpleCriterionClause(modifier, "?") clause, count := getSimpleCriterionClause(modifier, "?")

View File

@@ -594,7 +594,7 @@ func TestStringCriterionHandlerIsNull(t *testing.T) {
}, column)) }, column))
assert.Len(f.whereClauses, 1) 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) assert.Len(f.whereClauses[0].args, 0)
} }
@@ -609,6 +609,6 @@ func TestStringCriterionHandlerNotNull(t *testing.T) {
}, column)) }, column))
assert.Len(f.whereClauses, 1) 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) 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.handleStringCriterionInput(galleryFilter.Path, "galleries.path")
query.handleIntCriterionInput(galleryFilter.Rating, "galleries.rating") query.handleIntCriterionInput(galleryFilter.Rating, "galleries.rating")
query.handleStringCriterionInput(galleryFilter.URL, "galleries.url")
qb.handleAverageResolutionFilter(&query, galleryFilter.AverageResolution) qb.handleAverageResolutionFilter(&query, galleryFilter.AverageResolution)
if Organized := galleryFilter.Organized; Organized != nil { 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) { func TestGalleryQueryRating(t *testing.T) {
const rating = 3 const rating = 3
ratingCriterion := models.IntCriterionInput{ ratingCriterion := models.IntCriterionInput{

View File

@@ -122,11 +122,10 @@ func (qb *movieQueryBuilder) Query(movieFilter *models.MovieFilterType, findFilt
movieFilter = &models.MovieFilterType{} movieFilter = &models.MovieFilterType{}
} }
var whereClauses []string query := qb.newQuery()
var havingClauses []string
var args []interface{} query.body = selectDistinctIDs("movies")
body := selectDistinctIDs("movies") query.body += `
body += `
left join movies_scenes as scenes_join on scenes_join.movie_id = movies.id 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 scenes on scenes_join.scene_id = scenes.id
left join studios as studio on studio.id = movies.studio_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 != "" { if q := findFilter.Q; q != nil && *q != "" {
searchColumns := []string{"movies.name"} searchColumns := []string{"movies.name"}
clause, thisArgs := getSearchBinding(searchColumns, *q, false) clause, thisArgs := getSearchBinding(searchColumns, *q, false)
whereClauses = append(whereClauses, clause) query.addWhere(clause)
args = append(args, thisArgs...) query.addArg(thisArgs...)
} }
if studiosFilter := movieFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 { if studiosFilter := movieFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 {
for _, studioID := range studiosFilter.Value { for _, studioID := range studiosFilter.Value {
args = append(args, studioID) query.addArg(studioID)
} }
whereClause, havingClause := getMultiCriterionClause("movies", "studio", "", "", "studio_id", studiosFilter) whereClause, havingClause := getMultiCriterionClause("movies", "studio", "", "", "studio_id", studiosFilter)
whereClauses = appendClause(whereClauses, whereClause) query.addWhere(whereClause)
havingClauses = appendClause(havingClauses, havingClause) query.addHaving(havingClause)
} }
if isMissingFilter := movieFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" { if isMissingFilter := movieFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
switch *isMissingFilter { switch *isMissingFilter {
case "front_image": 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": 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": 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: default:
whereClauses = appendClause(whereClauses, "movies."+*isMissingFilter+" IS NULL") query.addWhere("movies." + *isMissingFilter + " IS NULL")
} }
} }
sortAndPagination := qb.getMovieSort(findFilter) + getPagination(findFilter) query.handleStringCriterionInput(movieFilter.URL, "movies.url")
idsResult, countResult, err := qb.executeFindQuery(body, args, sortAndPagination, whereClauses, havingClauses)
query.sortAndPagination = qb.getMovieSort(findFilter) + getPagination(findFilter)
idsResult, countResult, err := query.executeFind()
if err != nil { if err != nil {
return nil, 0, err 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) { func TestMovieUpdateMovieImages(t *testing.T) {
if err := withTxn(func(r models.Repository) error { if err := withTxn(func(r models.Repository) error {
mqb := r.Movie() 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.CareerLength, tableName+".career_length")
query.handleStringCriterionInput(performerFilter.Tattoos, tableName+".tattoos") query.handleStringCriterionInput(performerFilter.Tattoos, tableName+".tattoos")
query.handleStringCriterionInput(performerFilter.Piercings, tableName+".piercings") query.handleStringCriterionInput(performerFilter.Piercings, tableName+".piercings")
query.handleStringCriterionInput(performerFilter.URL, tableName+".url")
// TODO - need better handling of aliases // TODO - need better handling of aliases
query.handleStringCriterionInput(performerFilter.Aliases, tableName+".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 { func queryPerformers(t *testing.T, qb models.PerformerReader, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) []*models.Performer {
performers, _, err := qb.Query(performerFilter, findFilter) performers, _, err := qb.Query(performerFilter, findFilter)
if err != nil { 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(resolutionCriterionHandler(sceneFilter.Resolution, "scenes.height", "scenes.width"))
query.handleCriterionFunc(hasMarkersCriterionHandler(sceneFilter.HasMarkers)) query.handleCriterionFunc(hasMarkersCriterionHandler(sceneFilter.HasMarkers))
query.handleCriterionFunc(sceneIsMissingCriterionHandler(qb, sceneFilter.IsMissing)) query.handleCriterionFunc(sceneIsMissingCriterionHandler(qb, sceneFilter.IsMissing))
query.handleCriterionFunc(stringCriterionHandler(sceneFilter.URL, "scenes.url"))
query.handleCriterionFunc(sceneTagsCriterionHandler(qb, sceneFilter.Tags)) query.handleCriterionFunc(sceneTagsCriterionHandler(qb, sceneFilter.Tags))
query.handleCriterionFunc(scenePerformersCriterionHandler(qb, sceneFilter.Performers)) query.handleCriterionFunc(scenePerformersCriterionHandler(qb, sceneFilter.Performers))

View File

@@ -187,6 +187,44 @@ func TestSceneQueryPath(t *testing.T) {
verifyScenesPath(t, pathCriterion) 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) { func TestSceneQueryPathOr(t *testing.T) {
const scene1Idx = 1 const scene1Idx = 1
const scene2Idx = 2 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) { func verifyScenesPath(t *testing.T, pathCriterion models.StringCriterionInput) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
sqb := r.Scene() sqb := r.Scene()
@@ -345,10 +401,15 @@ func verifyNullString(t *testing.T, value sql.NullString, criterion models.Strin
t.Helper() t.Helper()
assert := assert.New(t) assert := assert.New(t)
if criterion.Modifier == models.CriterionModifierIsNull { if criterion.Modifier == models.CriterionModifierIsNull {
if value.Valid && value.String == "" {
// correct
return
}
assert.False(value.Valid, "expect is null values to be null") assert.False(value.Valid, "expect is null values to be null")
} }
if criterion.Modifier == models.CriterionModifierNotNull { if criterion.Modifier == models.CriterionModifierNotNull {
assert.True(value.Valid, "expect is null values to be null") assert.True(value.Valid, "expect is null values to be null")
assert.Greater(len(value.String), 0)
} }
if criterion.Modifier == models.CriterionModifierEquals { if criterion.Modifier == models.CriterionModifierEquals {
assert.Equal(criterion.Value, value.String) assert.Equal(criterion.Value, value.String)

View File

@@ -76,7 +76,8 @@ const (
movieIdxWithScene = iota movieIdxWithScene = iota
movieIdxWithStudio movieIdxWithStudio
// movies with dup names start from the end // 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 moviesNameCase = movieIdxWithDupName
moviesNameNoCase = 1 moviesNameNoCase = 1
@@ -146,6 +147,7 @@ const (
pathField = "Path" pathField = "Path"
checksumField = "Checksum" checksumField = "Checksum"
titleField = "Title" titleField = "Title"
urlField = "URL"
zipPath = "zipPath.zip" zipPath = "zipPath.zip"
) )
@@ -407,8 +409,32 @@ func populateDB() error {
return nil 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 { 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 { 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}, Title: sql.NullString{String: getSceneStringValue(i, titleField), Valid: true},
Checksum: sql.NullString{String: getSceneStringValue(i, checksumField), Valid: true}, Checksum: sql.NullString{String: getSceneStringValue(i, checksumField), Valid: true},
Details: sql.NullString{String: getSceneStringValue(i, "Details"), Valid: true}, Details: sql.NullString{String: getSceneStringValue(i, "Details"), Valid: true},
URL: getSceneNullStringValue(i, urlField),
Rating: getRating(i), Rating: getRating(i),
OCounter: getOCounter(i), OCounter: getOCounter(i),
Duration: getSceneDuration(i), Duration: getSceneDuration(i),
@@ -511,13 +538,18 @@ func createImages(qb models.ImageReaderWriter, n int) error {
} }
func getGalleryStringValue(index int, field string) string { 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 { func createGalleries(gqb models.GalleryReaderWriter, n int) error {
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
gallery := models.Gallery{ gallery := models.Gallery{
Path: models.NullString(getGalleryStringValue(i, pathField)), Path: models.NullString(getGalleryStringValue(i, pathField)),
URL: getGalleryNullStringValue(i, urlField),
Checksum: getGalleryStringValue(i, checksumField), Checksum: getGalleryStringValue(i, checksumField),
} }
@@ -534,7 +566,11 @@ func createGalleries(gqb models.GalleryReaderWriter, n int) error {
} }
func getMovieStringValue(index int, field string) string { func getMovieStringValue(index int, field string) string {
return "movie_" + strconv.FormatInt(int64(index), 10) + "_" + field return getPrefixedStringValue("movie", index, field)
}
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 // createMoviees creates n movies with plain Name and o movies with camel cased NaMe included
@@ -555,6 +591,7 @@ func createMovies(mqb models.MovieReaderWriter, n int, o int) error {
name = getMovieStringValue(index, name) name = getMovieStringValue(index, name)
movie := models.Movie{ movie := models.Movie{
Name: sql.NullString{String: name, Valid: true}, Name: sql.NullString{String: name, Valid: true},
URL: getMovieNullStringValue(index, urlField),
Checksum: utils.MD5FromString(name), 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 { 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 { func getPerformerBoolValue(index int) bool {
@@ -615,6 +656,7 @@ func createPerformers(pqb models.PerformerReaderWriter, n int, o int) error {
performer := models.Performer{ performer := models.Performer{
Name: sql.NullString{String: getPerformerStringValue(index, name), Valid: true}, Name: sql.NullString{String: getPerformerStringValue(index, name), Valid: true},
Checksum: getPerformerStringValue(i, checksumField), Checksum: getPerformerStringValue(i, checksumField),
URL: getPerformerNullStringValue(i, urlField),
Favorite: sql.NullBool{Bool: getPerformerBoolValue(i), Valid: true}, Favorite: sql.NullBool{Bool: getPerformerBoolValue(i), Valid: true},
Birthdate: models.SQLiteDate{ Birthdate: models.SQLiteDate{
String: getPerformerBirthdate(i), String: getPerformerBirthdate(i),
@@ -718,7 +760,11 @@ func createTags(tqb models.TagReaderWriter, n int, o int) error {
} }
func getStudioStringValue(index int, field string) string { 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) { 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} 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) created, err := sqb.Create(studio)
if err != nil { if err != nil {
@@ -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 // studios [ i ] and [ n + o - i - 1 ] should have similar names with only the Name!=NaMe part different
name = getStudioStringValue(index, name) 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 { if err != nil {
return err return err

View File

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

View File

@@ -3,7 +3,8 @@
* Added scene queue. * Added scene queue.
### 🎨 Improvements ### 🎨 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. * Support `today` and `yesterday` for `parseDate` in scrapers.
* Add random sorting option for galleries, studios, movies and tags. * Add random sorting option for galleries, studios, movies and tags.
* Disable sounds on scene/marker wall previews by default. * Disable sounds on scene/marker wall previews by default.

View File

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

View File

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

View File

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