mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
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:
@@ -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 {
|
||||
|
||||
@@ -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, "?")
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ export function makeCriteria(type: CriterionType = "none") {
|
||||
case "tattoos":
|
||||
case "piercings":
|
||||
case "aliases":
|
||||
case "url":
|
||||
return new StringCriterion(type, type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user