mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Add related object filter criteria to various filter types in graphql schema (#4861)
* Move filter criterion handlers into separate file * Add related filters for image filter * Add related filters for scene filter * Add related filters to gallery filter * Add related filters to movie filter * Add related filters to performer filter * Add related filters to studio filter * Add related filters to tag filter * Add scene filter to scene marker filter
This commit is contained in:
@@ -98,10 +98,12 @@ func getSingleLetterTags(ctx context.Context, c *Cache, reader models.TagAutoTag
|
||||
Value: singleFirstCharacterRegex,
|
||||
Modifier: models.CriterionModifierMatchesRegex,
|
||||
},
|
||||
Or: &models.TagFilterType{
|
||||
Aliases: &models.StringCriterionInput{
|
||||
Value: singleFirstCharacterRegex,
|
||||
Modifier: models.CriterionModifierMatchesRegex,
|
||||
OperatorFilter: models.OperatorFilter[models.TagFilterType]{
|
||||
Or: &models.TagFilterType{
|
||||
Aliases: &models.StringCriterionInput{
|
||||
Value: singleFirstCharacterRegex,
|
||||
Modifier: models.CriterionModifierMatchesRegex,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, &models.FindFilterType{
|
||||
|
||||
@@ -6,6 +6,27 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type OperatorFilter[T any] struct {
|
||||
And *T `json:"AND"`
|
||||
Or *T `json:"OR"`
|
||||
Not *T `json:"NOT"`
|
||||
}
|
||||
|
||||
// SubFilter returns the subfilter of the operator filter.
|
||||
// Only one of And, Or, or Not should be set, so it returns the first of these that are not nil.
|
||||
func (f *OperatorFilter[T]) SubFilter() *T {
|
||||
if f.And != nil {
|
||||
return f.And
|
||||
}
|
||||
if f.Or != nil {
|
||||
return f.Or
|
||||
}
|
||||
if f.Not != nil {
|
||||
return f.Not
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CriterionModifier string
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package models
|
||||
|
||||
type GalleryFilterType struct {
|
||||
And *GalleryFilterType `json:"AND"`
|
||||
Or *GalleryFilterType `json:"OR"`
|
||||
Not *GalleryFilterType `json:"NOT"`
|
||||
OperatorFilter[GalleryFilterType]
|
||||
ID *IntCriterionInput `json:"id"`
|
||||
Title *StringCriterionInput `json:"title"`
|
||||
Code *StringCriterionInput `json:"code"`
|
||||
@@ -51,6 +49,16 @@ type GalleryFilterType struct {
|
||||
URL *StringCriterionInput `json:"url"`
|
||||
// Filter by date
|
||||
Date *DateCriterionInput `json:"date"`
|
||||
// Filter by related scenes that meet this criteria
|
||||
ScenesFilter *SceneFilterType `json:"scenes_filter"`
|
||||
// Filter by related images that meet this criteria
|
||||
ImagesFilter *ImageFilterType `json:"images_filter"`
|
||||
// Filter by related performers that meet this criteria
|
||||
PerformersFilter *PerformerFilterType `json:"performers_filter"`
|
||||
// Filter by related studios that meet this criteria
|
||||
StudiosFilter *StudioFilterType `json:"studios_filter"`
|
||||
// Filter by related tags that meet this criteria
|
||||
TagsFilter *TagFilterType `json:"tags_filter"`
|
||||
// Filter by created at
|
||||
CreatedAt *TimestampCriterionInput `json:"created_at"`
|
||||
// Filter by updated at
|
||||
|
||||
@@ -3,9 +3,7 @@ package models
|
||||
import "context"
|
||||
|
||||
type ImageFilterType struct {
|
||||
And *ImageFilterType `json:"AND"`
|
||||
Or *ImageFilterType `json:"OR"`
|
||||
Not *ImageFilterType `json:"NOT"`
|
||||
OperatorFilter[ImageFilterType]
|
||||
ID *IntCriterionInput `json:"id"`
|
||||
Title *StringCriterionInput `json:"title"`
|
||||
Code *StringCriterionInput `json:"code"`
|
||||
@@ -51,6 +49,14 @@ type ImageFilterType struct {
|
||||
PerformerAge *IntCriterionInput `json:"performer_age"`
|
||||
// Filter to only include images with these galleries
|
||||
Galleries *MultiCriterionInput `json:"galleries"`
|
||||
// Filter by related galleries that meet this criteria
|
||||
GalleriesFilter *GalleryFilterType `json:"galleries_filter"`
|
||||
// Filter by related performers that meet this criteria
|
||||
PerformersFilter *PerformerFilterType `json:"performers_filter"`
|
||||
// Filter by related studios that meet this criteria
|
||||
StudiosFilter *StudioFilterType `json:"studios_filter"`
|
||||
// Filter by related tags that meet this criteria
|
||||
TagsFilter *TagFilterType `json:"tags_filter"`
|
||||
// Filter by created at
|
||||
CreatedAt *TimestampCriterionInput `json:"created_at"`
|
||||
// Filter by updated at
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package models
|
||||
|
||||
type MovieFilterType struct {
|
||||
OperatorFilter[MovieFilterType]
|
||||
Name *StringCriterionInput `json:"name"`
|
||||
Director *StringCriterionInput `json:"director"`
|
||||
Synopsis *StringCriterionInput `json:"synopsis"`
|
||||
@@ -18,6 +19,10 @@ type MovieFilterType struct {
|
||||
Performers *MultiCriterionInput `json:"performers"`
|
||||
// Filter by date
|
||||
Date *DateCriterionInput `json:"date"`
|
||||
// Filter by related scenes that meet this criteria
|
||||
ScenesFilter *SceneFilterType `json:"scenes_filter"`
|
||||
// Filter by related studios that meet this criteria
|
||||
StudiosFilter *StudioFilterType `json:"studios_filter"`
|
||||
// Filter by created at
|
||||
CreatedAt *TimestampCriterionInput `json:"created_at"`
|
||||
// Filter by updated at
|
||||
|
||||
@@ -108,9 +108,7 @@ type CircumcisionCriterionInput struct {
|
||||
}
|
||||
|
||||
type PerformerFilterType struct {
|
||||
And *PerformerFilterType `json:"AND"`
|
||||
Or *PerformerFilterType `json:"OR"`
|
||||
Not *PerformerFilterType `json:"NOT"`
|
||||
OperatorFilter[PerformerFilterType]
|
||||
Name *StringCriterionInput `json:"name"`
|
||||
Disambiguation *StringCriterionInput `json:"disambiguation"`
|
||||
Details *StringCriterionInput `json:"details"`
|
||||
@@ -188,6 +186,14 @@ type PerformerFilterType struct {
|
||||
Birthdate *DateCriterionInput `json:"birth_date"`
|
||||
// Filter by death date
|
||||
DeathDate *DateCriterionInput `json:"death_date"`
|
||||
// Filter by related scenes that meet this criteria
|
||||
ScenesFilter *SceneFilterType `json:"scenes_filter"`
|
||||
// Filter by related images that meet this criteria
|
||||
ImagesFilter *ImageFilterType `json:"images_filter"`
|
||||
// Filter by related galleries that meet this criteria
|
||||
GalleriesFilter *GalleryFilterType `json:"galleries_filter"`
|
||||
// Filter by related tags that meet this criteria
|
||||
TagsFilter *TagFilterType `json:"tags_filter"`
|
||||
// Filter by created at
|
||||
CreatedAt *TimestampCriterionInput `json:"created_at"`
|
||||
// Filter by updated at
|
||||
|
||||
@@ -9,9 +9,7 @@ type PHashDuplicationCriterionInput struct {
|
||||
}
|
||||
|
||||
type SceneFilterType struct {
|
||||
And *SceneFilterType `json:"AND"`
|
||||
Or *SceneFilterType `json:"OR"`
|
||||
Not *SceneFilterType `json:"NOT"`
|
||||
OperatorFilter[SceneFilterType]
|
||||
ID *IntCriterionInput `json:"id"`
|
||||
Title *StringCriterionInput `json:"title"`
|
||||
Code *StringCriterionInput `json:"code"`
|
||||
@@ -97,6 +95,18 @@ type SceneFilterType struct {
|
||||
LastPlayedAt *TimestampCriterionInput `json:"last_played_at"`
|
||||
// Filter by date
|
||||
Date *DateCriterionInput `json:"date"`
|
||||
// Filter by related galleries that meet this criteria
|
||||
GalleriesFilter *GalleryFilterType `json:"galleries_filter"`
|
||||
// Filter by related performers that meet this criteria
|
||||
PerformersFilter *PerformerFilterType `json:"performers_filter"`
|
||||
// Filter by related studios that meet this criteria
|
||||
StudiosFilter *StudioFilterType `json:"studios_filter"`
|
||||
// Filter by related tags that meet this criteria
|
||||
TagsFilter *TagFilterType `json:"tags_filter"`
|
||||
// Filter by related movies that meet this criteria
|
||||
MoviesFilter *MovieFilterType `json:"movies_filter"`
|
||||
// Filter by related markers that meet this criteria
|
||||
MarkersFilter *SceneMarkerFilterType `json:"markers_filter"`
|
||||
// Filter by created at
|
||||
CreatedAt *TimestampCriterionInput `json:"created_at"`
|
||||
// Filter by updated at
|
||||
|
||||
@@ -19,6 +19,8 @@ type SceneMarkerFilterType struct {
|
||||
SceneCreatedAt *TimestampCriterionInput `json:"scene_created_at"`
|
||||
// Filter by scenes updated at
|
||||
SceneUpdatedAt *TimestampCriterionInput `json:"scene_updated_at"`
|
||||
// Filter by related scenes that meet this criteria
|
||||
SceneFilter *SceneFilterType `json:"scene_filter"`
|
||||
}
|
||||
|
||||
type MarkerStringsResultType struct {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package models
|
||||
|
||||
type StudioFilterType struct {
|
||||
And *StudioFilterType `json:"AND"`
|
||||
Or *StudioFilterType `json:"OR"`
|
||||
Not *StudioFilterType `json:"NOT"`
|
||||
OperatorFilter[StudioFilterType]
|
||||
Name *StringCriterionInput `json:"name"`
|
||||
Details *StringCriterionInput `json:"details"`
|
||||
// Filter to only include studios with this parent studio
|
||||
@@ -32,6 +30,12 @@ type StudioFilterType struct {
|
||||
ChildCount *IntCriterionInput `json:"child_count"`
|
||||
// Filter by autotag ignore value
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
// Filter by related scenes that meet this criteria
|
||||
ScenesFilter *SceneFilterType `json:"scenes_filter"`
|
||||
// Filter by related images that meet this criteria
|
||||
ImagesFilter *ImageFilterType `json:"images_filter"`
|
||||
// Filter by related galleries that meet this criteria
|
||||
GalleriesFilter *GalleryFilterType `json:"galleries_filter"`
|
||||
// Filter by created at
|
||||
CreatedAt *TimestampCriterionInput `json:"created_at"`
|
||||
// Filter by updated at
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package models
|
||||
|
||||
type TagFilterType struct {
|
||||
And *TagFilterType `json:"AND"`
|
||||
Or *TagFilterType `json:"OR"`
|
||||
Not *TagFilterType `json:"NOT"`
|
||||
OperatorFilter[TagFilterType]
|
||||
// Filter by tag name
|
||||
Name *StringCriterionInput `json:"name"`
|
||||
// Filter by tag aliases
|
||||
@@ -34,6 +32,12 @@ type TagFilterType struct {
|
||||
ChildCount *IntCriterionInput `json:"child_count"`
|
||||
// Filter by autotag ignore value
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
// Filter by related scenes that meet this criteria
|
||||
ScenesFilter *SceneFilterType `json:"scenes_filter"`
|
||||
// Filter by related images that meet this criteria
|
||||
ImagesFilter *ImageFilterType `json:"images_filter"`
|
||||
// Filter by related galleries that meet this criteria
|
||||
GalleriesFilter *GalleryFilterType `json:"galleries_filter"`
|
||||
// Filter by created at
|
||||
CreatedAt *TimestampCriterionInput `json:"created_at"`
|
||||
// Filter by updated at
|
||||
|
||||
@@ -346,8 +346,8 @@ func (qb *BlobStore) delete(ctx context.Context, checksum string) error {
|
||||
}
|
||||
|
||||
type blobJoinQueryBuilder struct {
|
||||
repository
|
||||
blobStore *BlobStore
|
||||
repository repository
|
||||
blobStore *BlobStore
|
||||
|
||||
joinTable string
|
||||
}
|
||||
@@ -381,7 +381,7 @@ func (qb *blobJoinQueryBuilder) UpdateImage(ctx context.Context, id int, blobCol
|
||||
}
|
||||
|
||||
sqlQuery := fmt.Sprintf("UPDATE %s SET %s = ? WHERE id = ?", qb.joinTable, blobCol)
|
||||
if _, err := qb.tx.Exec(ctx, sqlQuery, checksum, id); err != nil {
|
||||
if _, err := dbWrapper.Exec(ctx, sqlQuery, checksum, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -428,7 +428,7 @@ func (qb *blobJoinQueryBuilder) DestroyImage(ctx context.Context, id int, blobCo
|
||||
}
|
||||
|
||||
updateQuery := fmt.Sprintf("UPDATE %s SET %s = NULL WHERE id = ?", qb.joinTable, blobCol)
|
||||
if _, err = qb.tx.Exec(ctx, updateQuery, id); err != nil {
|
||||
if _, err = dbWrapper.Exec(ctx, updateQuery, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -441,7 +441,7 @@ func (qb *blobJoinQueryBuilder) HasImage(ctx context.Context, id int, blobCol st
|
||||
"joinCol": blobCol,
|
||||
})
|
||||
|
||||
c, err := qb.runCountQuery(ctx, stmt, []interface{}{id})
|
||||
c, err := qb.repository.runCountQuery(ctx, stmt, []interface{}{id})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -61,7 +61,7 @@ func (e *MismatchedSchemaVersionError) Error() string {
|
||||
return fmt.Sprintf("schema version %d is incompatible with required schema version %d", e.CurrentSchemaVersion, e.RequiredSchemaVersion)
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
type storeRepository struct {
|
||||
Blobs *BlobStore
|
||||
File *FileStore
|
||||
Folder *FolderStore
|
||||
@@ -75,6 +75,10 @@ type Database struct {
|
||||
Studio *StudioStore
|
||||
Tag *TagStore
|
||||
Movie *MovieStore
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
*storeRepository
|
||||
|
||||
db *sqlx.DB
|
||||
dbPath string
|
||||
@@ -87,23 +91,32 @@ type Database struct {
|
||||
func NewDatabase() *Database {
|
||||
fileStore := NewFileStore()
|
||||
folderStore := NewFolderStore()
|
||||
galleryStore := NewGalleryStore(fileStore, folderStore)
|
||||
blobStore := NewBlobStore(BlobStoreOptions{})
|
||||
performerStore := NewPerformerStore(blobStore)
|
||||
studioStore := NewStudioStore(blobStore)
|
||||
tagStore := NewTagStore(blobStore)
|
||||
|
||||
ret := &Database{
|
||||
r := &storeRepository{}
|
||||
*r = storeRepository{
|
||||
Blobs: blobStore,
|
||||
File: fileStore,
|
||||
Folder: folderStore,
|
||||
Scene: NewSceneStore(fileStore, blobStore),
|
||||
Scene: NewSceneStore(r, blobStore),
|
||||
SceneMarker: NewSceneMarkerStore(),
|
||||
Image: NewImageStore(fileStore),
|
||||
Gallery: NewGalleryStore(fileStore, folderStore),
|
||||
Image: NewImageStore(r),
|
||||
Gallery: galleryStore,
|
||||
GalleryChapter: NewGalleryChapterStore(),
|
||||
Performer: NewPerformerStore(blobStore),
|
||||
Studio: NewStudioStore(blobStore),
|
||||
Tag: NewTagStore(blobStore),
|
||||
Performer: performerStore,
|
||||
Studio: studioStore,
|
||||
Tag: tagStore,
|
||||
Movie: NewMovieStore(blobStore),
|
||||
SavedFilter: NewSavedFilterStore(),
|
||||
lockChan: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
ret := &Database{
|
||||
storeRepository: r,
|
||||
lockChan: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -370,7 +383,7 @@ func (db *Database) Analyze(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (db *Database) ExecSQL(ctx context.Context, query string, args []interface{}) (*int64, *int64, error) {
|
||||
wrapper := dbWrapper{}
|
||||
wrapper := dbWrapperType{}
|
||||
|
||||
result, err := wrapper.Exec(ctx, query, args...)
|
||||
if err != nil {
|
||||
@@ -393,7 +406,7 @@ func (db *Database) ExecSQL(ctx context.Context, query string, args []interface{
|
||||
}
|
||||
|
||||
func (db *Database) QuerySQL(ctx context.Context, query string, args []interface{}) ([]string, [][]interface{}, error) {
|
||||
wrapper := dbWrapper{}
|
||||
wrapper := dbWrapperType{}
|
||||
|
||||
rows, err := wrapper.QueryxContext(ctx, query, args...)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
|
||||
@@ -947,7 +947,6 @@ func (qb *FileStore) setQuerySort(query *queryBuilder, findFilter *models.FindFi
|
||||
func (qb *FileStore) captionRepository() *captionRepository {
|
||||
return &captionRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: videoCaptionsTable,
|
||||
idColumn: fileIDColumn,
|
||||
},
|
||||
|
||||
@@ -2,19 +2,55 @@ package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func illegalFilterCombination(type1, type2 string) error {
|
||||
return fmt.Errorf("cannot have %s and %s in the same filter", type1, type2)
|
||||
}
|
||||
|
||||
func validateFilterCombination[T any](sf models.OperatorFilter[T]) error {
|
||||
const and = "AND"
|
||||
const or = "OR"
|
||||
const not = "NOT"
|
||||
|
||||
if sf.And != nil {
|
||||
if sf.Or != nil {
|
||||
return illegalFilterCombination(and, or)
|
||||
}
|
||||
if sf.Not != nil {
|
||||
return illegalFilterCombination(and, not)
|
||||
}
|
||||
}
|
||||
|
||||
if sf.Or != nil {
|
||||
if sf.Not != nil {
|
||||
return illegalFilterCombination(or, not)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleSubFilter[T any](ctx context.Context, handler criterionHandler, f *filterBuilder, subFilter models.OperatorFilter[T]) {
|
||||
subQuery := &filterBuilder{}
|
||||
handler.handle(ctx, subQuery)
|
||||
|
||||
if subFilter.And != nil {
|
||||
f.and(subQuery)
|
||||
}
|
||||
if subFilter.Or != nil {
|
||||
f.or(subQuery)
|
||||
}
|
||||
if subFilter.Not != nil {
|
||||
f.not(subQuery)
|
||||
}
|
||||
}
|
||||
|
||||
type sqlClause struct {
|
||||
sql string
|
||||
args []interface{}
|
||||
@@ -54,16 +90,6 @@ func andClauses(clauses ...sqlClause) sqlClause {
|
||||
return joinClauses("AND", clauses...)
|
||||
}
|
||||
|
||||
type criterionHandler interface {
|
||||
handle(ctx context.Context, f *filterBuilder)
|
||||
}
|
||||
|
||||
type criterionHandlerFunc func(ctx context.Context, f *filterBuilder)
|
||||
|
||||
func (h criterionHandlerFunc) handle(ctx context.Context, f *filterBuilder) {
|
||||
h(ctx, f)
|
||||
}
|
||||
|
||||
type join struct {
|
||||
table string
|
||||
as string
|
||||
@@ -143,6 +169,16 @@ type filterBuilder struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *filterBuilder) empty() bool {
|
||||
return f == nil || (len(f.whereClauses) == 0 && len(f.joins) == 0 && len(f.havingClauses) == 0 && f.subFilter == nil)
|
||||
}
|
||||
|
||||
func filterBuilderFromHandler(ctx context.Context, handler criterionHandler) *filterBuilder {
|
||||
f := &filterBuilder{}
|
||||
handler.handle(ctx, f)
|
||||
return f
|
||||
}
|
||||
|
||||
var errSubFilterAlreadySet = errors.New(`sub-filter already set`)
|
||||
|
||||
// sub-filter operator values
|
||||
@@ -388,876 +424,3 @@ func (f *filterBuilder) andClauses(input []sqlClause) (string, []interface{}) {
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func stringCriterionHandler(c *models.StringCriterionInput, column string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
if modifier := c.Modifier; c.Modifier.IsValid() {
|
||||
switch modifier {
|
||||
case models.CriterionModifierIncludes:
|
||||
f.whereClauses = append(f.whereClauses, getStringSearchClause([]string{column}, c.Value, false))
|
||||
case models.CriterionModifierExcludes:
|
||||
f.whereClauses = append(f.whereClauses, getStringSearchClause([]string{column}, c.Value, true))
|
||||
case models.CriterionModifierEquals:
|
||||
f.addWhere(column+" LIKE ?", c.Value)
|
||||
case models.CriterionModifierNotEquals:
|
||||
f.addWhere(column+" NOT LIKE ?", c.Value)
|
||||
case models.CriterionModifierMatchesRegex:
|
||||
if _, err := regexp.Compile(c.Value); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
f.addWhere(fmt.Sprintf("(%s IS NOT NULL AND %[1]s regexp ?)", column), c.Value)
|
||||
case models.CriterionModifierNotMatchesRegex:
|
||||
if _, err := regexp.Compile(c.Value); err != nil {
|
||||
f.setError(err)
|
||||
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:
|
||||
panic("unsupported string filter modifier")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func enumCriterionHandler(modifier models.CriterionModifier, values []string, column string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if modifier.IsValid() {
|
||||
switch modifier {
|
||||
case models.CriterionModifierIncludes, models.CriterionModifierEquals:
|
||||
if len(values) > 0 {
|
||||
f.whereClauses = append(f.whereClauses, getEnumSearchClause(column, values, false))
|
||||
}
|
||||
case models.CriterionModifierExcludes, models.CriterionModifierNotEquals:
|
||||
if len(values) > 0 {
|
||||
f.whereClauses = append(f.whereClauses, getEnumSearchClause(column, values, true))
|
||||
}
|
||||
case models.CriterionModifierIsNull:
|
||||
f.addWhere("(" + column + " IS NULL OR TRIM(" + column + ") = '')")
|
||||
case models.CriterionModifierNotNull:
|
||||
f.addWhere("(" + column + " IS NOT NULL AND TRIM(" + column + ") != '')")
|
||||
default:
|
||||
panic("unsupported string filter modifier")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pathCriterionHandler(c *models.StringCriterionInput, pathColumn string, basenameColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
if addJoinFn != nil {
|
||||
addJoinFn(f)
|
||||
}
|
||||
addWildcards := true
|
||||
not := false
|
||||
|
||||
if modifier := c.Modifier; c.Modifier.IsValid() {
|
||||
switch modifier {
|
||||
case models.CriterionModifierIncludes:
|
||||
f.whereClauses = append(f.whereClauses, getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not))
|
||||
case models.CriterionModifierExcludes:
|
||||
not = true
|
||||
f.whereClauses = append(f.whereClauses, getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not))
|
||||
case models.CriterionModifierEquals:
|
||||
addWildcards = false
|
||||
f.whereClauses = append(f.whereClauses, getPathSearchClause(pathColumn, basenameColumn, c.Value, addWildcards, not))
|
||||
case models.CriterionModifierNotEquals:
|
||||
addWildcards = false
|
||||
not = true
|
||||
f.whereClauses = append(f.whereClauses, getPathSearchClause(pathColumn, basenameColumn, c.Value, addWildcards, not))
|
||||
case models.CriterionModifierMatchesRegex:
|
||||
if _, err := regexp.Compile(c.Value); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
filepathColumn := fmt.Sprintf("%s || '%s' || %s", pathColumn, string(filepath.Separator), basenameColumn)
|
||||
f.addWhere(fmt.Sprintf("%s IS NOT NULL AND %s IS NOT NULL AND %s regexp ?", pathColumn, basenameColumn, filepathColumn), c.Value)
|
||||
case models.CriterionModifierNotMatchesRegex:
|
||||
if _, err := regexp.Compile(c.Value); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
filepathColumn := fmt.Sprintf("%s || '%s' || %s", pathColumn, string(filepath.Separator), basenameColumn)
|
||||
f.addWhere(fmt.Sprintf("%s IS NULL OR %s IS NULL OR %s NOT regexp ?", pathColumn, basenameColumn, filepathColumn), c.Value)
|
||||
case models.CriterionModifierIsNull:
|
||||
f.addWhere(fmt.Sprintf("%s IS NULL OR TRIM(%[1]s) = '' OR %s IS NULL OR TRIM(%[2]s) = ''", pathColumn, basenameColumn))
|
||||
case models.CriterionModifierNotNull:
|
||||
f.addWhere(fmt.Sprintf("%s IS NOT NULL AND TRIM(%[1]s) != '' AND %s IS NOT NULL AND TRIM(%[2]s) != ''", pathColumn, basenameColumn))
|
||||
default:
|
||||
panic("unsupported string filter modifier")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getPathSearchClause(pathColumn, basenameColumn, p string, addWildcards, not bool) sqlClause {
|
||||
if addWildcards {
|
||||
p = "%" + p + "%"
|
||||
}
|
||||
|
||||
filepathColumn := fmt.Sprintf("%s || '%s' || %s", pathColumn, string(filepath.Separator), basenameColumn)
|
||||
ret := makeClause(fmt.Sprintf("%s LIKE ?", filepathColumn), p)
|
||||
|
||||
if not {
|
||||
ret = ret.not()
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// getPathSearchClauseMany splits the query string p on whitespace
|
||||
// Used for backwards compatibility for the includes/excludes modifiers
|
||||
func getPathSearchClauseMany(pathColumn, basenameColumn, p string, addWildcards, not bool) sqlClause {
|
||||
q := strings.TrimSpace(p)
|
||||
trimmedQuery := strings.Trim(q, "\"")
|
||||
|
||||
if trimmedQuery == q {
|
||||
q = regexp.MustCompile(`\s+`).ReplaceAllString(q, " ")
|
||||
queryWords := strings.Split(q, " ")
|
||||
|
||||
var ret []sqlClause
|
||||
// Search for any word
|
||||
for _, word := range queryWords {
|
||||
ret = append(ret, getPathSearchClause(pathColumn, basenameColumn, word, addWildcards, not))
|
||||
}
|
||||
|
||||
if !not {
|
||||
return orClauses(ret...)
|
||||
}
|
||||
|
||||
return andClauses(ret...)
|
||||
}
|
||||
|
||||
return getPathSearchClause(pathColumn, basenameColumn, trimmedQuery, addWildcards, not)
|
||||
}
|
||||
|
||||
func intCriterionHandler(c *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
if addJoinFn != nil {
|
||||
addJoinFn(f)
|
||||
}
|
||||
clause, args := getIntCriterionWhereClause(column, *c)
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func floatCriterionHandler(c *models.FloatCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
if addJoinFn != nil {
|
||||
addJoinFn(f)
|
||||
}
|
||||
clause, args := getFloatCriterionWhereClause(column, *c)
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func boolCriterionHandler(c *bool, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
if addJoinFn != nil {
|
||||
addJoinFn(f)
|
||||
}
|
||||
var v string
|
||||
if *c {
|
||||
v = "1"
|
||||
} else {
|
||||
v = "0"
|
||||
}
|
||||
|
||||
f.addWhere(column + " = " + v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dateCriterionHandler(c *models.DateCriterionInput, column string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
clause, args := getDateCriterionWhereClause(column, *c)
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func timestampCriterionHandler(c *models.TimestampCriterionInput, column string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
clause, args := getTimestampCriterionWhereClause(column, *c)
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle for MultiCriterion where there is a join table between the new
|
||||
// objects
|
||||
type joinedMultiCriterionHandlerBuilder struct {
|
||||
// table containing the primary objects
|
||||
primaryTable string
|
||||
// table joining primary and foreign objects
|
||||
joinTable string
|
||||
// alias for join table, if required
|
||||
joinAs string
|
||||
// foreign key of the primary object on the join table
|
||||
primaryFK string
|
||||
// foreign key of the foreign object on the join table
|
||||
foreignFK string
|
||||
|
||||
addJoinTable func(f *filterBuilder)
|
||||
}
|
||||
|
||||
func (m *joinedMultiCriterionHandlerBuilder) handler(c *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
// make local copy so we can modify it
|
||||
criterion := *c
|
||||
|
||||
joinAlias := m.joinAs
|
||||
if joinAlias == "" {
|
||||
joinAlias = m.joinTable
|
||||
}
|
||||
|
||||
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if criterion.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
m.addJoinTable(f)
|
||||
|
||||
f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{
|
||||
"table": joinAlias,
|
||||
"column": m.foreignFK,
|
||||
"not": notClause,
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
if len(criterion.Value) == 0 && len(criterion.Excludes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// combine excludes if excludes modifier is selected
|
||||
if criterion.Modifier == models.CriterionModifierExcludes {
|
||||
criterion.Modifier = models.CriterionModifierIncludesAll
|
||||
criterion.Excludes = append(criterion.Excludes, criterion.Value...)
|
||||
criterion.Value = nil
|
||||
}
|
||||
|
||||
if len(criterion.Value) > 0 {
|
||||
whereClause := ""
|
||||
havingClause := ""
|
||||
|
||||
var args []interface{}
|
||||
for _, tagID := range criterion.Value {
|
||||
args = append(args, tagID)
|
||||
}
|
||||
|
||||
switch criterion.Modifier {
|
||||
case models.CriterionModifierIncludes:
|
||||
// includes any of the provided ids
|
||||
m.addJoinTable(f)
|
||||
whereClause = fmt.Sprintf("%s.%s IN %s", joinAlias, m.foreignFK, getInBinding(len(criterion.Value)))
|
||||
case models.CriterionModifierEquals:
|
||||
// includes only the provided ids
|
||||
m.addJoinTable(f)
|
||||
whereClause = utils.StrFormat("{joinAlias}.{foreignFK} IN {inBinding} AND (SELECT COUNT(*) FROM {joinTable} s WHERE s.{primaryFK} = {primaryTable}.id) = ?", utils.StrFormatMap{
|
||||
"joinAlias": joinAlias,
|
||||
"foreignFK": m.foreignFK,
|
||||
"inBinding": getInBinding(len(criterion.Value)),
|
||||
"joinTable": m.joinTable,
|
||||
"primaryFK": m.primaryFK,
|
||||
"primaryTable": m.primaryTable,
|
||||
})
|
||||
havingClause = fmt.Sprintf("count(distinct %s.%s) IS %d", joinAlias, m.foreignFK, len(criterion.Value))
|
||||
args = append(args, len(criterion.Value))
|
||||
case models.CriterionModifierNotEquals:
|
||||
f.setError(fmt.Errorf("not equals modifier is not supported for multi criterion input"))
|
||||
case models.CriterionModifierIncludesAll:
|
||||
// includes all of the provided ids
|
||||
m.addJoinTable(f)
|
||||
whereClause = fmt.Sprintf("%s.%s IN %s", joinAlias, m.foreignFK, getInBinding(len(criterion.Value)))
|
||||
havingClause = fmt.Sprintf("count(distinct %s.%s) IS %d", joinAlias, m.foreignFK, len(criterion.Value))
|
||||
}
|
||||
|
||||
f.addWhere(whereClause, args...)
|
||||
f.addHaving(havingClause)
|
||||
}
|
||||
|
||||
if len(criterion.Excludes) > 0 {
|
||||
var args []interface{}
|
||||
for _, tagID := range criterion.Excludes {
|
||||
args = append(args, tagID)
|
||||
}
|
||||
|
||||
// excludes all of the provided ids
|
||||
// need to use actual join table name for this
|
||||
// <primaryTable>.id NOT IN (select <joinTable>.<primaryFK> from <joinTable> where <joinTable>.<foreignFK> in <values>)
|
||||
whereClause := fmt.Sprintf("%[1]s.id NOT IN (SELECT %[3]s.%[2]s from %[3]s where %[3]s.%[4]s in %[5]s)", m.primaryTable, m.primaryFK, m.joinTable, m.foreignFK, getInBinding(len(criterion.Excludes)))
|
||||
|
||||
f.addWhere(whereClause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type multiCriterionHandlerBuilder struct {
|
||||
primaryTable string
|
||||
foreignTable string
|
||||
joinTable string
|
||||
primaryFK string
|
||||
foreignFK string
|
||||
|
||||
// function that will be called to perform any necessary joins
|
||||
addJoinsFunc func(f *filterBuilder)
|
||||
}
|
||||
|
||||
func (m *multiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if criterion != nil {
|
||||
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if criterion.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
table := m.primaryTable
|
||||
if m.joinTable != "" {
|
||||
table = m.joinTable
|
||||
f.addLeftJoin(table, "", fmt.Sprintf("%s.%s = %s.id", table, m.primaryFK, m.primaryTable))
|
||||
}
|
||||
|
||||
f.addWhere(fmt.Sprintf("%s.%s IS %s NULL", table, m.foreignFK, notClause))
|
||||
return
|
||||
}
|
||||
|
||||
if len(criterion.Value) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var args []interface{}
|
||||
for _, tagID := range criterion.Value {
|
||||
args = append(args, tagID)
|
||||
}
|
||||
|
||||
if m.addJoinsFunc != nil {
|
||||
m.addJoinsFunc(f)
|
||||
}
|
||||
|
||||
whereClause, havingClause := getMultiCriterionClause(m.primaryTable, m.foreignTable, m.joinTable, m.primaryFK, m.foreignFK, criterion)
|
||||
f.addWhere(whereClause, args...)
|
||||
f.addHaving(havingClause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type countCriterionHandlerBuilder struct {
|
||||
primaryTable string
|
||||
joinTable string
|
||||
primaryFK string
|
||||
}
|
||||
|
||||
func (m *countCriterionHandlerBuilder) handler(criterion *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if criterion != nil {
|
||||
clause, args := getCountCriterionClause(m.primaryTable, m.joinTable, m.primaryFK, *criterion)
|
||||
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handler for StringCriterion for string list fields
|
||||
type stringListCriterionHandlerBuilder struct {
|
||||
// table joining primary and foreign objects
|
||||
joinTable string
|
||||
// string field on the join table
|
||||
stringColumn string
|
||||
|
||||
addJoinTable func(f *filterBuilder)
|
||||
}
|
||||
|
||||
func (m *stringListCriterionHandlerBuilder) handler(criterion *models.StringCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if criterion != nil {
|
||||
m.addJoinTable(f)
|
||||
|
||||
stringCriterionHandler(criterion, m.joinTable+"."+m.stringColumn)(ctx, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func studioCriterionHandler(primaryTable string, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if studios == nil {
|
||||
return
|
||||
}
|
||||
|
||||
studiosCopy := *studios
|
||||
switch studiosCopy.Modifier {
|
||||
case models.CriterionModifierEquals:
|
||||
studiosCopy.Modifier = models.CriterionModifierIncludesAll
|
||||
case models.CriterionModifierNotEquals:
|
||||
studiosCopy.Modifier = models.CriterionModifierExcludes
|
||||
}
|
||||
|
||||
hh := hierarchicalMultiCriterionHandlerBuilder{
|
||||
tx: dbWrapper{},
|
||||
|
||||
primaryTable: primaryTable,
|
||||
foreignTable: studioTable,
|
||||
foreignFK: studioIDColumn,
|
||||
parentFK: "parent_id",
|
||||
}
|
||||
|
||||
hh.handler(&studiosCopy)(ctx, f)
|
||||
}
|
||||
}
|
||||
|
||||
type hierarchicalMultiCriterionHandlerBuilder struct {
|
||||
tx dbWrapper
|
||||
|
||||
primaryTable string
|
||||
foreignTable string
|
||||
foreignFK string
|
||||
|
||||
parentFK string
|
||||
childFK string
|
||||
relationsTable string
|
||||
}
|
||||
|
||||
func getHierarchicalValues(ctx context.Context, tx dbWrapper, values []string, table, relationsTable, parentFK string, childFK string, depth *int) (string, error) {
|
||||
var args []interface{}
|
||||
|
||||
if parentFK == "" {
|
||||
parentFK = "parent_id"
|
||||
}
|
||||
if childFK == "" {
|
||||
childFK = "child_id"
|
||||
}
|
||||
|
||||
depthVal := 0
|
||||
if depth != nil {
|
||||
depthVal = *depth
|
||||
}
|
||||
|
||||
if depthVal == 0 {
|
||||
valid := true
|
||||
var valuesClauses []string
|
||||
for _, value := range values {
|
||||
id, err := strconv.Atoi(value)
|
||||
// In case of invalid value just run the query.
|
||||
// Building VALUES() based on provided values just saves a query when depth is 0.
|
||||
if err != nil {
|
||||
valid = false
|
||||
break
|
||||
}
|
||||
|
||||
valuesClauses = append(valuesClauses, fmt.Sprintf("(%d,%d)", id, id))
|
||||
}
|
||||
|
||||
if valid {
|
||||
return "VALUES" + strings.Join(valuesClauses, ","), nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
args = append(args, value)
|
||||
}
|
||||
inCount := len(args)
|
||||
|
||||
var depthCondition string
|
||||
if depthVal != -1 {
|
||||
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
|
||||
}
|
||||
|
||||
withClauseMap := utils.StrFormatMap{
|
||||
"table": table,
|
||||
"relationsTable": relationsTable,
|
||||
"inBinding": getInBinding(inCount),
|
||||
"recursiveSelect": "",
|
||||
"parentFK": parentFK,
|
||||
"childFK": childFK,
|
||||
"depthCondition": depthCondition,
|
||||
"unionClause": "",
|
||||
}
|
||||
|
||||
if relationsTable != "" {
|
||||
withClauseMap["recursiveSelect"] = utils.StrFormat(`SELECT p.root_id, c.{childFK}, depth + 1 FROM {relationsTable} AS c
|
||||
INNER JOIN items as p ON c.{parentFK} = p.item_id
|
||||
`, withClauseMap)
|
||||
} else {
|
||||
withClauseMap["recursiveSelect"] = utils.StrFormat(`SELECT p.root_id, c.id, depth + 1 FROM {table} as c
|
||||
INNER JOIN items as p ON c.{parentFK} = p.item_id
|
||||
`, withClauseMap)
|
||||
}
|
||||
|
||||
if depthVal != 0 {
|
||||
withClauseMap["unionClause"] = utils.StrFormat(`
|
||||
UNION {recursiveSelect} {depthCondition}
|
||||
`, withClauseMap)
|
||||
}
|
||||
|
||||
withClause := utils.StrFormat(`items AS (
|
||||
SELECT id as root_id, id as item_id, 0 as depth FROM {table}
|
||||
WHERE id in {inBinding}
|
||||
{unionClause})
|
||||
`, withClauseMap)
|
||||
|
||||
query := fmt.Sprintf("WITH RECURSIVE %s SELECT 'VALUES' || GROUP_CONCAT('(' || root_id || ', ' || item_id || ')') AS val FROM items", withClause)
|
||||
|
||||
var valuesClause sql.NullString
|
||||
err := tx.Get(ctx, &valuesClause, query, args...)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get hierarchical values: %w", err)
|
||||
}
|
||||
|
||||
// if no values are found, just return a values string with the values only
|
||||
if !valuesClause.Valid {
|
||||
for i, value := range values {
|
||||
values[i] = fmt.Sprintf("(%s, %s)", value, value)
|
||||
}
|
||||
valuesClause.String = "VALUES" + strings.Join(values, ",")
|
||||
}
|
||||
|
||||
return valuesClause.String, nil
|
||||
}
|
||||
|
||||
func addHierarchicalConditionClauses(f *filterBuilder, criterion models.HierarchicalMultiCriterionInput, table, idColumn string) {
|
||||
switch criterion.Modifier {
|
||||
case models.CriterionModifierIncludes:
|
||||
f.addWhere(fmt.Sprintf("%s.%s IS NOT NULL", table, idColumn))
|
||||
case models.CriterionModifierIncludesAll:
|
||||
f.addWhere(fmt.Sprintf("%s.%s IS NOT NULL", table, idColumn))
|
||||
f.addHaving(fmt.Sprintf("count(distinct %s.%s) IS %d", table, idColumn, len(criterion.Value)))
|
||||
case models.CriterionModifierExcludes:
|
||||
f.addWhere(fmt.Sprintf("%s.%s IS NULL", table, idColumn))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *hierarchicalMultiCriterionHandlerBuilder) handler(c *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
// make a copy so we don't modify the original
|
||||
criterion := *c
|
||||
|
||||
// don't support equals/not equals
|
||||
if criterion.Modifier == models.CriterionModifierEquals || criterion.Modifier == models.CriterionModifierNotEquals {
|
||||
f.setError(fmt.Errorf("modifier %s is not supported for hierarchical multi criterion", criterion.Modifier))
|
||||
return
|
||||
}
|
||||
|
||||
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if criterion.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{
|
||||
"table": m.primaryTable,
|
||||
"column": m.foreignFK,
|
||||
"not": notClause,
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
if len(criterion.Value) == 0 && len(criterion.Excludes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// combine excludes if excludes modifier is selected
|
||||
if criterion.Modifier == models.CriterionModifierExcludes {
|
||||
criterion.Modifier = models.CriterionModifierIncludesAll
|
||||
criterion.Excludes = append(criterion.Excludes, criterion.Value...)
|
||||
criterion.Value = nil
|
||||
}
|
||||
|
||||
if len(criterion.Value) > 0 {
|
||||
valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
|
||||
if err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
switch criterion.Modifier {
|
||||
case models.CriterionModifierIncludes:
|
||||
f.addWhere(fmt.Sprintf("%s.%s IN (SELECT column2 FROM (%s))", m.primaryTable, m.foreignFK, valuesClause))
|
||||
case models.CriterionModifierIncludesAll:
|
||||
f.addWhere(fmt.Sprintf("%s.%s IN (SELECT column2 FROM (%s))", m.primaryTable, m.foreignFK, valuesClause))
|
||||
f.addHaving(fmt.Sprintf("count(distinct %s.%s) IS %d", m.primaryTable, m.foreignFK, len(criterion.Value)))
|
||||
}
|
||||
}
|
||||
|
||||
if len(criterion.Excludes) > 0 {
|
||||
valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Excludes, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
|
||||
if err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
f.addWhere(fmt.Sprintf("%s.%s NOT IN (SELECT column2 FROM (%s)) OR %[1]s.%[2]s IS NULL", m.primaryTable, m.foreignFK, valuesClause))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type joinedHierarchicalMultiCriterionHandlerBuilder struct {
|
||||
tx dbWrapper
|
||||
|
||||
primaryTable string
|
||||
primaryKey string
|
||||
foreignTable string
|
||||
foreignFK string
|
||||
|
||||
parentFK string
|
||||
childFK string
|
||||
relationsTable string
|
||||
|
||||
joinAs string
|
||||
joinTable string
|
||||
primaryFK string
|
||||
}
|
||||
|
||||
func (m *joinedHierarchicalMultiCriterionHandlerBuilder) addHierarchicalConditionClauses(f *filterBuilder, criterion models.HierarchicalMultiCriterionInput, table, idColumn string) {
|
||||
primaryKey := m.primaryKey
|
||||
if primaryKey == "" {
|
||||
primaryKey = "id"
|
||||
}
|
||||
|
||||
switch criterion.Modifier {
|
||||
case models.CriterionModifierEquals:
|
||||
// includes only the provided ids
|
||||
f.addWhere(fmt.Sprintf("%s.%s IS NOT NULL", table, idColumn))
|
||||
f.addHaving(fmt.Sprintf("count(distinct %s.%s) IS %d", table, idColumn, len(criterion.Value)))
|
||||
f.addWhere(utils.StrFormat("(SELECT COUNT(*) FROM {joinTable} s WHERE s.{primaryFK} = {primaryTable}.{primaryKey}) = ?", utils.StrFormatMap{
|
||||
"joinTable": m.joinTable,
|
||||
"primaryFK": m.primaryFK,
|
||||
"primaryTable": m.primaryTable,
|
||||
"primaryKey": primaryKey,
|
||||
}), len(criterion.Value))
|
||||
case models.CriterionModifierNotEquals:
|
||||
f.setError(fmt.Errorf("not equals modifier is not supported for hierarchical multi criterion input"))
|
||||
default:
|
||||
addHierarchicalConditionClauses(f, criterion, table, idColumn)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
// make a copy so we don't modify the original
|
||||
criterion := *c
|
||||
joinAlias := m.joinAs
|
||||
primaryKey := m.primaryKey
|
||||
if primaryKey == "" {
|
||||
primaryKey = "id"
|
||||
}
|
||||
|
||||
if criterion.Modifier == models.CriterionModifierEquals && criterion.Depth != nil && *criterion.Depth != 0 {
|
||||
f.setError(fmt.Errorf("depth is not supported for equals modifier in hierarchical multi criterion input"))
|
||||
return
|
||||
}
|
||||
|
||||
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if criterion.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
f.addLeftJoin(m.joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.%s", joinAlias, m.primaryFK, m.primaryTable, primaryKey))
|
||||
|
||||
f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{
|
||||
"table": joinAlias,
|
||||
"column": m.foreignFK,
|
||||
"not": notClause,
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
// combine excludes if excludes modifier is selected
|
||||
if criterion.Modifier == models.CriterionModifierExcludes {
|
||||
criterion.Modifier = models.CriterionModifierIncludesAll
|
||||
criterion.Excludes = append(criterion.Excludes, criterion.Value...)
|
||||
criterion.Value = nil
|
||||
}
|
||||
|
||||
if len(criterion.Value) == 0 && len(criterion.Excludes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(criterion.Value) > 0 {
|
||||
valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
|
||||
if err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
joinTable := utils.StrFormat(`(
|
||||
SELECT j.*, d.column1 AS root_id, d.column2 AS item_id FROM {joinTable} AS j
|
||||
INNER JOIN ({valuesClause}) AS d ON j.{foreignFK} = d.column2
|
||||
)
|
||||
`, utils.StrFormatMap{
|
||||
"joinTable": m.joinTable,
|
||||
"foreignFK": m.foreignFK,
|
||||
"valuesClause": valuesClause,
|
||||
})
|
||||
|
||||
f.addLeftJoin(joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.%s", joinAlias, m.primaryFK, m.primaryTable, primaryKey))
|
||||
|
||||
m.addHierarchicalConditionClauses(f, criterion, joinAlias, "root_id")
|
||||
}
|
||||
|
||||
if len(criterion.Excludes) > 0 {
|
||||
valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Excludes, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
|
||||
if err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
joinTable := utils.StrFormat(`(
|
||||
SELECT j2.*, e.column1 AS root_id, e.column2 AS item_id FROM {joinTable} AS j2
|
||||
INNER JOIN ({valuesClause}) AS e ON j2.{foreignFK} = e.column2
|
||||
)
|
||||
`, utils.StrFormatMap{
|
||||
"joinTable": m.joinTable,
|
||||
"foreignFK": m.foreignFK,
|
||||
"valuesClause": valuesClause,
|
||||
})
|
||||
|
||||
joinAlias2 := joinAlias + "2"
|
||||
|
||||
f.addLeftJoin(joinTable, joinAlias2, fmt.Sprintf("%s.%s = %s.%s", joinAlias2, m.primaryFK, m.primaryTable, primaryKey))
|
||||
|
||||
// modify for exclusion
|
||||
criterionCopy := criterion
|
||||
criterionCopy.Modifier = models.CriterionModifierExcludes
|
||||
criterionCopy.Value = c.Excludes
|
||||
|
||||
m.addHierarchicalConditionClauses(f, criterionCopy, joinAlias2, "root_id")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type joinedPerformerTagsHandler struct {
|
||||
criterion *models.HierarchicalMultiCriterionInput
|
||||
|
||||
primaryTable string // eg scenes
|
||||
joinTable string // eg performers_scenes
|
||||
joinPrimaryKey string // eg scene_id
|
||||
}
|
||||
|
||||
func (h *joinedPerformerTagsHandler) handle(ctx context.Context, f *filterBuilder) {
|
||||
tags := h.criterion
|
||||
|
||||
if tags != nil {
|
||||
criterion := tags.CombineExcludes()
|
||||
|
||||
// validate the modifier
|
||||
switch criterion.Modifier {
|
||||
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
|
||||
// valid
|
||||
default:
|
||||
f.setError(fmt.Errorf("invalid modifier %s for performer tags", criterion.Modifier))
|
||||
}
|
||||
|
||||
strFormatMap := utils.StrFormatMap{
|
||||
"primaryTable": h.primaryTable,
|
||||
"joinTable": h.joinTable,
|
||||
"joinPrimaryKey": h.joinPrimaryKey,
|
||||
"inBinding": getInBinding(len(criterion.Value)),
|
||||
}
|
||||
|
||||
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if criterion.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
f.addLeftJoin(h.joinTable, "", utils.StrFormat("{primaryTable}.id = {joinTable}.{joinPrimaryKey}", strFormatMap))
|
||||
f.addLeftJoin("performers_tags", "", utils.StrFormat("{joinTable}.performer_id = performers_tags.performer_id", strFormatMap))
|
||||
|
||||
f.addWhere(fmt.Sprintf("performers_tags.tag_id IS %s NULL", notClause))
|
||||
return
|
||||
}
|
||||
|
||||
if len(criterion.Value) == 0 && len(criterion.Excludes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(criterion.Value) > 0 {
|
||||
valuesClause, err := getHierarchicalValues(ctx, dbWrapper{}, criterion.Value, tagTable, "tags_relations", "", "", criterion.Depth)
|
||||
if err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
f.addWith(utils.StrFormat(`performer_tags AS (
|
||||
SELECT ps.{joinPrimaryKey} as primaryID, t.column1 AS root_tag_id FROM {joinTable} ps
|
||||
INNER JOIN performers_tags pt ON pt.performer_id = ps.performer_id
|
||||
INNER JOIN (`+valuesClause+`) t ON t.column2 = pt.tag_id
|
||||
)`, strFormatMap))
|
||||
|
||||
f.addLeftJoin("performer_tags", "", utils.StrFormat("performer_tags.primaryID = {primaryTable}.id", strFormatMap))
|
||||
|
||||
addHierarchicalConditionClauses(f, criterion, "performer_tags", "root_tag_id")
|
||||
}
|
||||
|
||||
if len(criterion.Excludes) > 0 {
|
||||
valuesClause, err := getHierarchicalValues(ctx, dbWrapper{}, criterion.Excludes, tagTable, "tags_relations", "", "", criterion.Depth)
|
||||
if err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
clause := utils.StrFormat("{primaryTable}.id NOT IN (SELECT {joinTable}.{joinPrimaryKey} FROM {joinTable} INNER JOIN performers_tags ON {joinTable}.performer_id = performers_tags.performer_id WHERE performers_tags.tag_id IN (SELECT column2 FROM (%s)))", strFormatMap)
|
||||
f.addWhere(fmt.Sprintf(clause, valuesClause))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type stashIDCriterionHandler struct {
|
||||
c *models.StashIDCriterionInput
|
||||
stashIDRepository *stashIDRepository
|
||||
stashIDTableAs string
|
||||
parentIDCol string
|
||||
}
|
||||
|
||||
func (h *stashIDCriterionHandler) handle(ctx context.Context, f *filterBuilder) {
|
||||
if h.c == nil {
|
||||
return
|
||||
}
|
||||
|
||||
stashIDRepo := h.stashIDRepository
|
||||
t := stashIDRepo.tableName
|
||||
if h.stashIDTableAs != "" {
|
||||
t = h.stashIDTableAs
|
||||
}
|
||||
|
||||
joinClause := fmt.Sprintf("%s.%s = %s", t, stashIDRepo.idColumn, h.parentIDCol)
|
||||
if h.c.Endpoint != nil && *h.c.Endpoint != "" {
|
||||
joinClause += fmt.Sprintf(" AND %s.endpoint = '%s'", t, *h.c.Endpoint)
|
||||
}
|
||||
|
||||
f.addLeftJoin(stashIDRepo.tableName, h.stashIDTableAs, joinClause)
|
||||
|
||||
v := ""
|
||||
if h.c.StashID != nil {
|
||||
v = *h.c.StashID
|
||||
}
|
||||
|
||||
stringCriterionHandler(&models.StringCriterionInput{
|
||||
Value: v,
|
||||
Modifier: h.c.Modifier,
|
||||
}, t+".stash_id")(ctx, f)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/doug-martin/goqu/v9/exp"
|
||||
@@ -113,9 +112,75 @@ func (r *galleryRowRecord) fromPartial(o models.GalleryPartial) {
|
||||
r.setTimestamp("updated_at", o.UpdatedAt)
|
||||
}
|
||||
|
||||
type GalleryStore struct {
|
||||
type galleryRepositoryType struct {
|
||||
repository
|
||||
performers joinRepository
|
||||
images joinRepository
|
||||
tags joinRepository
|
||||
scenes joinRepository
|
||||
files filesRepository
|
||||
}
|
||||
|
||||
func (r *galleryRepositoryType) addGalleriesFilesTable(f *filterBuilder) {
|
||||
f.addLeftJoin(galleriesFilesTable, "", "galleries_files.gallery_id = galleries.id")
|
||||
}
|
||||
|
||||
func (r *galleryRepositoryType) addFilesTable(f *filterBuilder) {
|
||||
r.addGalleriesFilesTable(f)
|
||||
f.addLeftJoin(fileTable, "", "galleries_files.file_id = files.id")
|
||||
}
|
||||
|
||||
func (r *galleryRepositoryType) addFoldersTable(f *filterBuilder) {
|
||||
r.addFilesTable(f)
|
||||
f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id")
|
||||
}
|
||||
|
||||
var (
|
||||
galleryRepository = galleryRepositoryType{
|
||||
repository: repository{
|
||||
tableName: galleryTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
performers: joinRepository{
|
||||
repository: repository{
|
||||
tableName: performersGalleriesTable,
|
||||
idColumn: galleryIDColumn,
|
||||
},
|
||||
fkColumn: "performer_id",
|
||||
},
|
||||
tags: joinRepository{
|
||||
repository: repository{
|
||||
tableName: galleriesTagsTable,
|
||||
idColumn: galleryIDColumn,
|
||||
},
|
||||
fkColumn: "tag_id",
|
||||
foreignTable: tagTable,
|
||||
orderBy: "tags.name ASC",
|
||||
},
|
||||
images: joinRepository{
|
||||
repository: repository{
|
||||
tableName: galleriesImagesTable,
|
||||
idColumn: galleryIDColumn,
|
||||
},
|
||||
fkColumn: "image_id",
|
||||
},
|
||||
scenes: joinRepository{
|
||||
repository: repository{
|
||||
tableName: galleriesScenesTable,
|
||||
idColumn: galleryIDColumn,
|
||||
},
|
||||
fkColumn: sceneIDColumn,
|
||||
},
|
||||
files: filesRepository{
|
||||
repository: repository{
|
||||
tableName: galleriesFilesTable,
|
||||
idColumn: galleryIDColumn,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type GalleryStore struct {
|
||||
tableMgr *table
|
||||
|
||||
fileStore *FileStore
|
||||
@@ -124,10 +189,6 @@ type GalleryStore struct {
|
||||
|
||||
func NewGalleryStore(fileStore *FileStore, folderStore *FolderStore) *GalleryStore {
|
||||
return &GalleryStore{
|
||||
repository: repository{
|
||||
tableName: galleryTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
tableMgr: galleryTableMgr,
|
||||
fileStore: fileStore,
|
||||
folderStore: folderStore,
|
||||
@@ -309,7 +370,7 @@ func (qb *GalleryStore) Destroy(ctx context.Context, id int) error {
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) GetFiles(ctx context.Context, id int) ([]models.File, error) {
|
||||
fileIDs, err := qb.filesRepository().get(ctx, id)
|
||||
fileIDs, err := galleryRepository.files.get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -328,7 +389,7 @@ func (qb *GalleryStore) GetFiles(ctx context.Context, id int) ([]models.File, er
|
||||
|
||||
func (qb *GalleryStore) GetManyFileIDs(ctx context.Context, ids []int) ([][]models.FileID, error) {
|
||||
const primaryOnly = false
|
||||
return qb.filesRepository().getMany(ctx, ids, primaryOnly)
|
||||
return galleryRepository.files.getMany(ctx, ids, primaryOnly)
|
||||
}
|
||||
|
||||
// returns nil, nil if not found
|
||||
@@ -617,116 +678,6 @@ func (qb *GalleryStore) All(ctx context.Context) ([]*models.Gallery, error) {
|
||||
return qb.getMany(ctx, qb.selectDataset())
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) validateFilter(galleryFilter *models.GalleryFilterType) error {
|
||||
const and = "AND"
|
||||
const or = "OR"
|
||||
const not = "NOT"
|
||||
|
||||
if galleryFilter.And != nil {
|
||||
if galleryFilter.Or != nil {
|
||||
return illegalFilterCombination(and, or)
|
||||
}
|
||||
if galleryFilter.Not != nil {
|
||||
return illegalFilterCombination(and, not)
|
||||
}
|
||||
|
||||
return qb.validateFilter(galleryFilter.And)
|
||||
}
|
||||
|
||||
if galleryFilter.Or != nil {
|
||||
if galleryFilter.Not != nil {
|
||||
return illegalFilterCombination(or, not)
|
||||
}
|
||||
|
||||
return qb.validateFilter(galleryFilter.Or)
|
||||
}
|
||||
|
||||
if galleryFilter.Not != nil {
|
||||
return qb.validateFilter(galleryFilter.Not)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.GalleryFilterType) *filterBuilder {
|
||||
query := &filterBuilder{}
|
||||
|
||||
if galleryFilter.And != nil {
|
||||
query.and(qb.makeFilter(ctx, galleryFilter.And))
|
||||
}
|
||||
if galleryFilter.Or != nil {
|
||||
query.or(qb.makeFilter(ctx, galleryFilter.Or))
|
||||
}
|
||||
if galleryFilter.Not != nil {
|
||||
query.not(qb.makeFilter(ctx, galleryFilter.Not))
|
||||
}
|
||||
|
||||
query.handleCriterion(ctx, intCriterionHandler(galleryFilter.ID, "galleries.id", nil))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.Title, "galleries.title"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.Code, "galleries.code"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.Details, "galleries.details"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.Photographer, "galleries.photographer"))
|
||||
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if galleryFilter.Checksum != nil {
|
||||
qb.addGalleriesFilesTable(f)
|
||||
f.addLeftJoin(fingerprintTable, "fingerprints_md5", "galleries_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
|
||||
}
|
||||
|
||||
stringCriterionHandler(galleryFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
|
||||
}))
|
||||
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if galleryFilter.IsZip != nil {
|
||||
qb.addGalleriesFilesTable(f)
|
||||
if *galleryFilter.IsZip {
|
||||
|
||||
f.addWhere("galleries_files.file_id IS NOT NULL")
|
||||
} else {
|
||||
f.addWhere("galleries_files.file_id IS NULL")
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
query.handleCriterion(ctx, qb.galleryPathCriterionHandler(galleryFilter.Path))
|
||||
query.handleCriterion(ctx, galleryFileCountCriterionHandler(qb, galleryFilter.FileCount))
|
||||
query.handleCriterion(ctx, intCriterionHandler(galleryFilter.Rating100, "galleries.rating", nil))
|
||||
query.handleCriterion(ctx, galleryURLsCriterionHandler(galleryFilter.URL))
|
||||
query.handleCriterion(ctx, boolCriterionHandler(galleryFilter.Organized, "galleries.organized", nil))
|
||||
query.handleCriterion(ctx, galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing))
|
||||
query.handleCriterion(ctx, galleryTagsCriterionHandler(qb, galleryFilter.Tags))
|
||||
query.handleCriterion(ctx, galleryTagCountCriterionHandler(qb, galleryFilter.TagCount))
|
||||
query.handleCriterion(ctx, galleryPerformersCriterionHandler(qb, galleryFilter.Performers))
|
||||
query.handleCriterion(ctx, galleryPerformerCountCriterionHandler(qb, galleryFilter.PerformerCount))
|
||||
query.handleCriterion(ctx, hasChaptersCriterionHandler(galleryFilter.HasChapters))
|
||||
query.handleCriterion(ctx, galleryScenesCriterionHandler(qb, galleryFilter.Scenes))
|
||||
query.handleCriterion(ctx, studioCriterionHandler(galleryTable, galleryFilter.Studios))
|
||||
query.handleCriterion(ctx, galleryPerformerTagsCriterionHandler(qb, galleryFilter.PerformerTags))
|
||||
query.handleCriterion(ctx, galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution))
|
||||
query.handleCriterion(ctx, galleryImageCountCriterionHandler(qb, galleryFilter.ImageCount))
|
||||
query.handleCriterion(ctx, galleryPerformerFavoriteCriterionHandler(galleryFilter.PerformerFavorite))
|
||||
query.handleCriterion(ctx, galleryPerformerAgeCriterionHandler(galleryFilter.PerformerAge))
|
||||
query.handleCriterion(ctx, dateCriterionHandler(galleryFilter.Date, "galleries.date"))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(galleryFilter.CreatedAt, "galleries.created_at"))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(galleryFilter.UpdatedAt, "galleries.updated_at"))
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) addGalleriesFilesTable(f *filterBuilder) {
|
||||
f.addLeftJoin(galleriesFilesTable, "", "galleries_files.gallery_id = galleries.id")
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) addFilesTable(f *filterBuilder) {
|
||||
qb.addGalleriesFilesTable(f)
|
||||
f.addLeftJoin(fileTable, "", "galleries_files.file_id = files.id")
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) addFoldersTable(f *filterBuilder) {
|
||||
qb.addFilesTable(f)
|
||||
f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id")
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) makeQuery(ctx context.Context, galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
|
||||
if galleryFilter == nil {
|
||||
galleryFilter = &models.GalleryFilterType{}
|
||||
@@ -735,7 +686,7 @@ func (qb *GalleryStore) makeQuery(ctx context.Context, galleryFilter *models.Gal
|
||||
findFilter = &models.FindFilterType{}
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
query := galleryRepository.newQuery()
|
||||
distinctIDs(&query, galleryTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
@@ -773,10 +724,9 @@ func (qb *GalleryStore) makeQuery(ctx context.Context, galleryFilter *models.Gal
|
||||
query.parseQueryString(searchColumns, *q)
|
||||
}
|
||||
|
||||
if err := qb.validateFilter(galleryFilter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter := qb.makeFilter(ctx, galleryFilter)
|
||||
filter := filterBuilderFromHandler(ctx, &galleryFilterHandler{
|
||||
galleryFilter: galleryFilter,
|
||||
})
|
||||
|
||||
if err := query.addFilter(filter); err != nil {
|
||||
return nil, err
|
||||
@@ -818,290 +768,6 @@ func (qb *GalleryStore) QueryCount(ctx context.Context, galleryFilter *models.Ga
|
||||
return query.executeCount(ctx)
|
||||
}
|
||||
|
||||
func galleryURLsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: galleriesURLsTable,
|
||||
stringColumn: galleriesURLColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
galleriesURLsTableMgr.join(f, "", "galleries.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(url)
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
|
||||
return multiCriterionHandlerBuilder{
|
||||
primaryTable: galleryTable,
|
||||
foreignTable: foreignTable,
|
||||
joinTable: joinTable,
|
||||
primaryFK: galleryIDColumn,
|
||||
foreignFK: foreignFK,
|
||||
addJoinsFunc: addJoinsFunc,
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) galleryPathCriterionHandler(c *models.StringCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
qb.addFoldersTable(f)
|
||||
f.addLeftJoin(folderTable, "gallery_folder", "galleries.folder_id = gallery_folder.id")
|
||||
|
||||
const pathColumn = "folders.path"
|
||||
const basenameColumn = "files.basename"
|
||||
const folderPathColumn = "gallery_folder.path"
|
||||
|
||||
addWildcards := true
|
||||
not := false
|
||||
|
||||
if modifier := c.Modifier; c.Modifier.IsValid() {
|
||||
switch modifier {
|
||||
case models.CriterionModifierIncludes:
|
||||
clause := getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not)
|
||||
clause2 := getStringSearchClause([]string{folderPathColumn}, c.Value, false)
|
||||
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
|
||||
case models.CriterionModifierExcludes:
|
||||
not = true
|
||||
clause := getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not)
|
||||
clause2 := getStringSearchClause([]string{folderPathColumn}, c.Value, true)
|
||||
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
|
||||
case models.CriterionModifierEquals:
|
||||
addWildcards = false
|
||||
clause := getPathSearchClause(pathColumn, basenameColumn, c.Value, addWildcards, not)
|
||||
clause2 := makeClause(folderPathColumn+" LIKE ?", c.Value)
|
||||
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
|
||||
case models.CriterionModifierNotEquals:
|
||||
addWildcards = false
|
||||
not = true
|
||||
clause := getPathSearchClause(pathColumn, basenameColumn, c.Value, addWildcards, not)
|
||||
clause2 := makeClause(folderPathColumn+" NOT LIKE ?", c.Value)
|
||||
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
|
||||
case models.CriterionModifierMatchesRegex:
|
||||
if _, err := regexp.Compile(c.Value); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
filepathColumn := fmt.Sprintf("%s || '%s' || %s", pathColumn, string(filepath.Separator), basenameColumn)
|
||||
clause := makeClause(fmt.Sprintf("%s IS NOT NULL AND %s IS NOT NULL AND %s regexp ?", pathColumn, basenameColumn, filepathColumn), c.Value)
|
||||
clause2 := makeClause(fmt.Sprintf("%s IS NOT NULL AND %[1]s regexp ?", folderPathColumn), c.Value)
|
||||
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
|
||||
case models.CriterionModifierNotMatchesRegex:
|
||||
if _, err := regexp.Compile(c.Value); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
filepathColumn := fmt.Sprintf("%s || '%s' || %s", pathColumn, string(filepath.Separator), basenameColumn)
|
||||
f.addWhere(fmt.Sprintf("%s IS NULL OR %s IS NULL OR %s NOT regexp ?", pathColumn, basenameColumn, filepathColumn), c.Value)
|
||||
f.addWhere(fmt.Sprintf("%s IS NULL OR %[1]s NOT regexp ?", folderPathColumn), c.Value)
|
||||
case models.CriterionModifierIsNull:
|
||||
f.addWhere(fmt.Sprintf("%s IS NULL OR TRIM(%[1]s) = '' OR %s IS NULL OR TRIM(%[2]s) = ''", pathColumn, basenameColumn))
|
||||
f.addWhere(fmt.Sprintf("%s IS NULL OR TRIM(%[1]s) = ''", folderPathColumn))
|
||||
case models.CriterionModifierNotNull:
|
||||
clause := makeClause(fmt.Sprintf("%s IS NOT NULL AND TRIM(%[1]s) != '' AND %s IS NOT NULL AND TRIM(%[2]s) != ''", pathColumn, basenameColumn))
|
||||
clause2 := makeClause(fmt.Sprintf("%s IS NOT NULL AND TRIM(%[1]s) != ''", folderPathColumn))
|
||||
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
|
||||
default:
|
||||
panic("unsupported string filter modifier")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func galleryFileCountCriterionHandler(qb *GalleryStore, fileCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: galleryTable,
|
||||
joinTable: galleriesFilesTable,
|
||||
primaryFK: galleryIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(fileCount)
|
||||
}
|
||||
|
||||
func galleryIsMissingCriterionHandler(qb *GalleryStore, isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "url":
|
||||
galleriesURLsTableMgr.join(f, "", "galleries.id")
|
||||
f.addWhere("gallery_urls.url IS NULL")
|
||||
case "scenes":
|
||||
f.addLeftJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id")
|
||||
f.addWhere("scenes_join.gallery_id IS NULL")
|
||||
case "studio":
|
||||
f.addWhere("galleries.studio_id IS NULL")
|
||||
case "performers":
|
||||
qb.performersRepository().join(f, "performers_join", "galleries.id")
|
||||
f.addWhere("performers_join.gallery_id IS NULL")
|
||||
case "date":
|
||||
f.addWhere("galleries.date IS NULL OR galleries.date IS \"\"")
|
||||
case "tags":
|
||||
qb.tagsRepository().join(f, "tags_join", "galleries.id")
|
||||
f.addWhere("tags_join.gallery_id IS NULL")
|
||||
default:
|
||||
f.addWhere("(galleries." + *isMissing + " IS NULL OR TRIM(galleries." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func galleryTagsCriterionHandler(qb *GalleryStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedHierarchicalMultiCriterionHandlerBuilder{
|
||||
tx: qb.tx,
|
||||
|
||||
primaryTable: galleryTable,
|
||||
foreignTable: tagTable,
|
||||
foreignFK: "tag_id",
|
||||
|
||||
relationsTable: "tags_relations",
|
||||
joinAs: "image_tag",
|
||||
joinTable: galleriesTagsTable,
|
||||
primaryFK: galleryIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(tags)
|
||||
}
|
||||
|
||||
func galleryTagCountCriterionHandler(qb *GalleryStore, tagCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: galleryTable,
|
||||
joinTable: galleriesTagsTable,
|
||||
primaryFK: galleryIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(tagCount)
|
||||
}
|
||||
|
||||
func galleryScenesCriterionHandler(qb *GalleryStore, scenes *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
qb.scenesRepository().join(f, "", "galleries.id")
|
||||
f.addLeftJoin("scenes", "", "scenes_galleries.scene_id = scenes.id")
|
||||
}
|
||||
h := qb.getMultiCriterionHandlerBuilder(sceneTable, galleriesScenesTable, "scene_id", addJoinsFunc)
|
||||
return h.handler(scenes)
|
||||
}
|
||||
|
||||
func galleryPerformersCriterionHandler(qb *GalleryStore, performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedMultiCriterionHandlerBuilder{
|
||||
primaryTable: galleryTable,
|
||||
joinTable: performersGalleriesTable,
|
||||
joinAs: "performers_join",
|
||||
primaryFK: galleryIDColumn,
|
||||
foreignFK: performerIDColumn,
|
||||
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
qb.performersRepository().join(f, "performers_join", "galleries.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(performers)
|
||||
}
|
||||
|
||||
func galleryPerformerCountCriterionHandler(qb *GalleryStore, performerCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: galleryTable,
|
||||
joinTable: performersGalleriesTable,
|
||||
primaryFK: galleryIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(performerCount)
|
||||
}
|
||||
|
||||
func galleryImageCountCriterionHandler(qb *GalleryStore, imageCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: galleryTable,
|
||||
joinTable: galleriesImagesTable,
|
||||
primaryFK: galleryIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(imageCount)
|
||||
}
|
||||
|
||||
func hasChaptersCriterionHandler(hasChapters *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if hasChapters != nil {
|
||||
f.addLeftJoin("galleries_chapters", "", "galleries_chapters.gallery_id = galleries.id")
|
||||
if *hasChapters == "true" {
|
||||
f.addHaving("count(galleries_chapters.gallery_id) > 0")
|
||||
} else {
|
||||
f.addWhere("galleries_chapters.id IS NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func galleryPerformerTagsCriterionHandler(qb *GalleryStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
|
||||
return &joinedPerformerTagsHandler{
|
||||
criterion: tags,
|
||||
primaryTable: galleryTable,
|
||||
joinTable: performersGalleriesTable,
|
||||
joinPrimaryKey: galleryIDColumn,
|
||||
}
|
||||
}
|
||||
|
||||
func galleryPerformerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performerfavorite != nil {
|
||||
f.addLeftJoin("performers_galleries", "", "galleries.id = performers_galleries.gallery_id")
|
||||
|
||||
if *performerfavorite {
|
||||
// contains at least one favorite
|
||||
f.addLeftJoin("performers", "", "performers.id = performers_galleries.performer_id")
|
||||
f.addWhere("performers.favorite = 1")
|
||||
} else {
|
||||
// contains zero favorites
|
||||
f.addLeftJoin(`(SELECT performers_galleries.gallery_id as id FROM performers_galleries
|
||||
JOIN performers ON performers.id = performers_galleries.performer_id
|
||||
GROUP BY performers_galleries.gallery_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "galleries.id = nofaves.id")
|
||||
f.addWhere("performers_galleries.gallery_id IS NULL OR nofaves.id IS NOT NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func galleryPerformerAgeCriterionHandler(performerAge *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performerAge != nil {
|
||||
f.addInnerJoin("performers_galleries", "", "galleries.id = performers_galleries.gallery_id")
|
||||
f.addInnerJoin("performers", "", "performers_galleries.performer_id = performers.id")
|
||||
|
||||
f.addWhere("galleries.date != '' AND performers.birthdate != ''")
|
||||
f.addWhere("galleries.date IS NOT NULL AND performers.birthdate IS NOT NULL")
|
||||
|
||||
ageCalc := "cast(strftime('%Y.%m%d', galleries.date) - strftime('%Y.%m%d', performers.birthdate) as int)"
|
||||
whereClause, args := getIntWhereClause(ageCalc, performerAge.Modifier, performerAge.Value, performerAge.Value2)
|
||||
f.addWhere(whereClause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func galleryAverageResolutionCriterionHandler(qb *GalleryStore, resolution *models.ResolutionCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if resolution != nil && resolution.Value.IsValid() {
|
||||
qb.imagesRepository().join(f, "images_join", "galleries.id")
|
||||
f.addLeftJoin("images", "", "images_join.image_id = images.id")
|
||||
f.addLeftJoin("images_files", "", "images.id = images_files.image_id")
|
||||
f.addLeftJoin("image_files", "", "images_files.file_id = image_files.file_id")
|
||||
|
||||
min := resolution.Value.GetMinResolution()
|
||||
max := resolution.Value.GetMaxResolution()
|
||||
|
||||
const widthHeight = "avg(MIN(image_files.width, image_files.height))"
|
||||
|
||||
switch resolution.Modifier {
|
||||
case models.CriterionModifierEquals:
|
||||
f.addHaving(fmt.Sprintf("%s BETWEEN %d AND %d", widthHeight, min, max))
|
||||
case models.CriterionModifierNotEquals:
|
||||
f.addHaving(fmt.Sprintf("%s NOT BETWEEN %d AND %d", widthHeight, min, max))
|
||||
case models.CriterionModifierLessThan:
|
||||
f.addHaving(fmt.Sprintf("%s < %d", widthHeight, min))
|
||||
case models.CriterionModifierGreaterThan:
|
||||
f.addHaving(fmt.Sprintf("%s > %d", widthHeight, max))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var gallerySortOptions = sortOptions{
|
||||
"created_at",
|
||||
"date",
|
||||
@@ -1194,92 +860,36 @@ func (qb *GalleryStore) GetURLs(ctx context.Context, galleryID int) ([]string, e
|
||||
return galleriesURLsTableMgr.get(ctx, galleryID)
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) filesRepository() *filesRepository {
|
||||
return &filesRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: galleriesFilesTable,
|
||||
idColumn: galleryIDColumn,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) AddFileID(ctx context.Context, id int, fileID models.FileID) error {
|
||||
const firstPrimary = false
|
||||
return galleriesFilesTableMgr.insertJoins(ctx, id, firstPrimary, []models.FileID{fileID})
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) performersRepository() *joinRepository {
|
||||
return &joinRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: performersGalleriesTable,
|
||||
idColumn: galleryIDColumn,
|
||||
},
|
||||
fkColumn: "performer_id",
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) GetPerformerIDs(ctx context.Context, id int) ([]int, error) {
|
||||
return qb.performersRepository().getIDs(ctx, id)
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) tagsRepository() *joinRepository {
|
||||
return &joinRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: galleriesTagsTable,
|
||||
idColumn: galleryIDColumn,
|
||||
},
|
||||
fkColumn: "tag_id",
|
||||
foreignTable: tagTable,
|
||||
orderBy: "tags.name ASC",
|
||||
}
|
||||
return galleryRepository.performers.getIDs(ctx, id)
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) GetTagIDs(ctx context.Context, id int) ([]int, error) {
|
||||
return qb.tagsRepository().getIDs(ctx, id)
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) imagesRepository() *joinRepository {
|
||||
return &joinRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: galleriesImagesTable,
|
||||
idColumn: galleryIDColumn,
|
||||
},
|
||||
fkColumn: "image_id",
|
||||
}
|
||||
return galleryRepository.tags.getIDs(ctx, id)
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) GetImageIDs(ctx context.Context, galleryID int) ([]int, error) {
|
||||
return qb.imagesRepository().getIDs(ctx, galleryID)
|
||||
return galleryRepository.images.getIDs(ctx, galleryID)
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) AddImages(ctx context.Context, galleryID int, imageIDs ...int) error {
|
||||
return qb.imagesRepository().insertOrIgnore(ctx, galleryID, imageIDs...)
|
||||
return galleryRepository.images.insertOrIgnore(ctx, galleryID, imageIDs...)
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) RemoveImages(ctx context.Context, galleryID int, imageIDs ...int) error {
|
||||
return qb.imagesRepository().destroyJoins(ctx, galleryID, imageIDs...)
|
||||
return galleryRepository.images.destroyJoins(ctx, galleryID, imageIDs...)
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) UpdateImages(ctx context.Context, galleryID int, imageIDs []int) error {
|
||||
// Delete the existing joins and then create new ones
|
||||
return qb.imagesRepository().replace(ctx, galleryID, imageIDs)
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) scenesRepository() *joinRepository {
|
||||
return &joinRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: galleriesScenesTable,
|
||||
idColumn: galleryIDColumn,
|
||||
},
|
||||
fkColumn: sceneIDColumn,
|
||||
}
|
||||
return galleryRepository.images.replace(ctx, galleryID, imageIDs)
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) GetSceneIDs(ctx context.Context, id int) ([]int, error) {
|
||||
return qb.scenesRepository().getIDs(ctx, id)
|
||||
return galleryRepository.scenes.getIDs(ctx, id)
|
||||
}
|
||||
|
||||
432
pkg/sqlite/gallery_filter.go
Normal file
432
pkg/sqlite/gallery_filter.go
Normal file
@@ -0,0 +1,432 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type galleryFilterHandler struct {
|
||||
galleryFilter *models.GalleryFilterType
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) validate() error {
|
||||
galleryFilter := qb.galleryFilter
|
||||
if galleryFilter == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := validateFilterCombination(galleryFilter.OperatorFilter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if subFilter := galleryFilter.SubFilter(); subFilter != nil {
|
||||
sqb := &galleryFilterHandler{galleryFilter: subFilter}
|
||||
if err := sqb.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) handle(ctx context.Context, f *filterBuilder) {
|
||||
galleryFilter := qb.galleryFilter
|
||||
if galleryFilter == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := qb.validate(); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
sf := galleryFilter.SubFilter()
|
||||
if sf != nil {
|
||||
sub := &galleryFilterHandler{sf}
|
||||
handleSubFilter(ctx, sub, f, galleryFilter.OperatorFilter)
|
||||
}
|
||||
|
||||
f.handleCriterion(ctx, qb.criterionHandler())
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) criterionHandler() criterionHandler {
|
||||
filter := qb.galleryFilter
|
||||
return compoundHandler{
|
||||
intCriterionHandler(filter.ID, "galleries.id", nil),
|
||||
stringCriterionHandler(filter.Title, "galleries.title"),
|
||||
stringCriterionHandler(filter.Code, "galleries.code"),
|
||||
stringCriterionHandler(filter.Details, "galleries.details"),
|
||||
stringCriterionHandler(filter.Photographer, "galleries.photographer"),
|
||||
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if filter.Checksum != nil {
|
||||
galleryRepository.addGalleriesFilesTable(f)
|
||||
f.addLeftJoin(fingerprintTable, "fingerprints_md5", "galleries_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
|
||||
}
|
||||
|
||||
stringCriterionHandler(filter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
|
||||
}),
|
||||
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if filter.IsZip != nil {
|
||||
galleryRepository.addGalleriesFilesTable(f)
|
||||
if *filter.IsZip {
|
||||
|
||||
f.addWhere("galleries_files.file_id IS NOT NULL")
|
||||
} else {
|
||||
f.addWhere("galleries_files.file_id IS NULL")
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
qb.pathCriterionHandler(filter.Path),
|
||||
qb.fileCountCriterionHandler(filter.FileCount),
|
||||
intCriterionHandler(filter.Rating100, "galleries.rating", nil),
|
||||
qb.urlsCriterionHandler(filter.URL),
|
||||
boolCriterionHandler(filter.Organized, "galleries.organized", nil),
|
||||
qb.missingCriterionHandler(filter.IsMissing),
|
||||
qb.tagsCriterionHandler(filter.Tags),
|
||||
qb.tagCountCriterionHandler(filter.TagCount),
|
||||
qb.performersCriterionHandler(filter.Performers),
|
||||
qb.performerCountCriterionHandler(filter.PerformerCount),
|
||||
qb.scenesCriterionHandler(filter.Scenes),
|
||||
qb.hasChaptersCriterionHandler(filter.HasChapters),
|
||||
studioCriterionHandler(galleryTable, filter.Studios),
|
||||
qb.performerTagsCriterionHandler(filter.PerformerTags),
|
||||
qb.averageResolutionCriterionHandler(filter.AverageResolution),
|
||||
qb.imageCountCriterionHandler(filter.ImageCount),
|
||||
qb.performerFavoriteCriterionHandler(filter.PerformerFavorite),
|
||||
qb.performerAgeCriterionHandler(filter.PerformerAge),
|
||||
&dateCriterionHandler{filter.Date, "galleries.date", nil},
|
||||
×tampCriterionHandler{filter.CreatedAt, "galleries.created_at", nil},
|
||||
×tampCriterionHandler{filter.UpdatedAt, "galleries.updated_at", nil},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "scenes_galleries.scene_id",
|
||||
relatedRepo: sceneRepository.repository,
|
||||
relatedHandler: &sceneFilterHandler{filter.ScenesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
galleryRepository.scenes.innerJoin(f, "", "galleries.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "galleries_images.image_id",
|
||||
relatedRepo: imageRepository.repository,
|
||||
relatedHandler: &imageFilterHandler{filter.ImagesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
galleryRepository.images.innerJoin(f, "", "galleries.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "performers_join.performer_id",
|
||||
relatedRepo: performerRepository.repository,
|
||||
relatedHandler: &performerFilterHandler{filter.PerformersFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
galleryRepository.performers.innerJoin(f, "performers_join", "galleries.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "galleries.studio_id",
|
||||
relatedRepo: studioRepository.repository,
|
||||
relatedHandler: &studioFilterHandler{filter.StudiosFilter},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "gallery_tag.tag_id",
|
||||
relatedRepo: tagRepository.repository,
|
||||
relatedHandler: &tagFilterHandler{filter.TagsFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
galleryRepository.tags.innerJoin(f, "gallery_tag", "galleries.id")
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) urlsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: galleriesURLsTable,
|
||||
stringColumn: galleriesURLColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
galleriesURLsTableMgr.join(f, "", "galleries.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(url)
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
|
||||
return multiCriterionHandlerBuilder{
|
||||
primaryTable: galleryTable,
|
||||
foreignTable: foreignTable,
|
||||
joinTable: joinTable,
|
||||
primaryFK: galleryIDColumn,
|
||||
foreignFK: foreignFK,
|
||||
addJoinsFunc: addJoinsFunc,
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) pathCriterionHandler(c *models.StringCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
galleryRepository.addFoldersTable(f)
|
||||
f.addLeftJoin(folderTable, "gallery_folder", "galleries.folder_id = gallery_folder.id")
|
||||
|
||||
const pathColumn = "folders.path"
|
||||
const basenameColumn = "files.basename"
|
||||
const folderPathColumn = "gallery_folder.path"
|
||||
|
||||
addWildcards := true
|
||||
not := false
|
||||
|
||||
if modifier := c.Modifier; c.Modifier.IsValid() {
|
||||
switch modifier {
|
||||
case models.CriterionModifierIncludes:
|
||||
clause := getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not)
|
||||
clause2 := getStringSearchClause([]string{folderPathColumn}, c.Value, false)
|
||||
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
|
||||
case models.CriterionModifierExcludes:
|
||||
not = true
|
||||
clause := getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not)
|
||||
clause2 := getStringSearchClause([]string{folderPathColumn}, c.Value, true)
|
||||
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
|
||||
case models.CriterionModifierEquals:
|
||||
addWildcards = false
|
||||
clause := getPathSearchClause(pathColumn, basenameColumn, c.Value, addWildcards, not)
|
||||
clause2 := makeClause(folderPathColumn+" LIKE ?", c.Value)
|
||||
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
|
||||
case models.CriterionModifierNotEquals:
|
||||
addWildcards = false
|
||||
not = true
|
||||
clause := getPathSearchClause(pathColumn, basenameColumn, c.Value, addWildcards, not)
|
||||
clause2 := makeClause(folderPathColumn+" NOT LIKE ?", c.Value)
|
||||
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
|
||||
case models.CriterionModifierMatchesRegex:
|
||||
if _, err := regexp.Compile(c.Value); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
filepathColumn := fmt.Sprintf("%s || '%s' || %s", pathColumn, string(filepath.Separator), basenameColumn)
|
||||
clause := makeClause(fmt.Sprintf("%s IS NOT NULL AND %s IS NOT NULL AND %s regexp ?", pathColumn, basenameColumn, filepathColumn), c.Value)
|
||||
clause2 := makeClause(fmt.Sprintf("%s IS NOT NULL AND %[1]s regexp ?", folderPathColumn), c.Value)
|
||||
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
|
||||
case models.CriterionModifierNotMatchesRegex:
|
||||
if _, err := regexp.Compile(c.Value); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
filepathColumn := fmt.Sprintf("%s || '%s' || %s", pathColumn, string(filepath.Separator), basenameColumn)
|
||||
f.addWhere(fmt.Sprintf("%s IS NULL OR %s IS NULL OR %s NOT regexp ?", pathColumn, basenameColumn, filepathColumn), c.Value)
|
||||
f.addWhere(fmt.Sprintf("%s IS NULL OR %[1]s NOT regexp ?", folderPathColumn), c.Value)
|
||||
case models.CriterionModifierIsNull:
|
||||
f.addWhere(fmt.Sprintf("%s IS NULL OR TRIM(%[1]s) = '' OR %s IS NULL OR TRIM(%[2]s) = ''", pathColumn, basenameColumn))
|
||||
f.addWhere(fmt.Sprintf("%s IS NULL OR TRIM(%[1]s) = ''", folderPathColumn))
|
||||
case models.CriterionModifierNotNull:
|
||||
clause := makeClause(fmt.Sprintf("%s IS NOT NULL AND TRIM(%[1]s) != '' AND %s IS NOT NULL AND TRIM(%[2]s) != ''", pathColumn, basenameColumn))
|
||||
clause2 := makeClause(fmt.Sprintf("%s IS NOT NULL AND TRIM(%[1]s) != ''", folderPathColumn))
|
||||
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
|
||||
default:
|
||||
panic("unsupported string filter modifier")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) fileCountCriterionHandler(fileCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: galleryTable,
|
||||
joinTable: galleriesFilesTable,
|
||||
primaryFK: galleryIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(fileCount)
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) missingCriterionHandler(isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "url":
|
||||
galleriesURLsTableMgr.join(f, "", "galleries.id")
|
||||
f.addWhere("gallery_urls.url IS NULL")
|
||||
case "scenes":
|
||||
f.addLeftJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id")
|
||||
f.addWhere("scenes_join.gallery_id IS NULL")
|
||||
case "studio":
|
||||
f.addWhere("galleries.studio_id IS NULL")
|
||||
case "performers":
|
||||
galleryRepository.performers.join(f, "performers_join", "galleries.id")
|
||||
f.addWhere("performers_join.gallery_id IS NULL")
|
||||
case "date":
|
||||
f.addWhere("galleries.date IS NULL OR galleries.date IS \"\"")
|
||||
case "tags":
|
||||
galleryRepository.tags.join(f, "tags_join", "galleries.id")
|
||||
f.addWhere("tags_join.gallery_id IS NULL")
|
||||
default:
|
||||
f.addWhere("(galleries." + *isMissing + " IS NULL OR TRIM(galleries." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) tagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedHierarchicalMultiCriterionHandlerBuilder{
|
||||
primaryTable: galleryTable,
|
||||
foreignTable: tagTable,
|
||||
foreignFK: "tag_id",
|
||||
|
||||
relationsTable: "tags_relations",
|
||||
joinAs: "gallery_tag",
|
||||
joinTable: galleriesTagsTable,
|
||||
primaryFK: galleryIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(tags)
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) tagCountCriterionHandler(tagCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: galleryTable,
|
||||
joinTable: galleriesTagsTable,
|
||||
primaryFK: galleryIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(tagCount)
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) scenesCriterionHandler(scenes *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
galleryRepository.scenes.join(f, "", "galleries.id")
|
||||
f.addLeftJoin("scenes", "", "scenes_galleries.scene_id = scenes.id")
|
||||
}
|
||||
h := qb.getMultiCriterionHandlerBuilder(sceneTable, galleriesScenesTable, "scene_id", addJoinsFunc)
|
||||
return h.handler(scenes)
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) performersCriterionHandler(performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedMultiCriterionHandlerBuilder{
|
||||
primaryTable: galleryTable,
|
||||
joinTable: performersGalleriesTable,
|
||||
joinAs: "performers_join",
|
||||
primaryFK: galleryIDColumn,
|
||||
foreignFK: performerIDColumn,
|
||||
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
galleryRepository.performers.join(f, "performers_join", "galleries.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(performers)
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) performerCountCriterionHandler(performerCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: galleryTable,
|
||||
joinTable: performersGalleriesTable,
|
||||
primaryFK: galleryIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(performerCount)
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) imageCountCriterionHandler(imageCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: galleryTable,
|
||||
joinTable: galleriesImagesTable,
|
||||
primaryFK: galleryIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(imageCount)
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) hasChaptersCriterionHandler(hasChapters *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if hasChapters != nil {
|
||||
f.addLeftJoin("galleries_chapters", "", "galleries_chapters.gallery_id = galleries.id")
|
||||
if *hasChapters == "true" {
|
||||
f.addHaving("count(galleries_chapters.gallery_id) > 0")
|
||||
} else {
|
||||
f.addWhere("galleries_chapters.id IS NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) performerTagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandler {
|
||||
return &joinedPerformerTagsHandler{
|
||||
criterion: tags,
|
||||
primaryTable: galleryTable,
|
||||
joinTable: performersGalleriesTable,
|
||||
joinPrimaryKey: galleryIDColumn,
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) performerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performerfavorite != nil {
|
||||
f.addLeftJoin("performers_galleries", "", "galleries.id = performers_galleries.gallery_id")
|
||||
|
||||
if *performerfavorite {
|
||||
// contains at least one favorite
|
||||
f.addLeftJoin("performers", "", "performers.id = performers_galleries.performer_id")
|
||||
f.addWhere("performers.favorite = 1")
|
||||
} else {
|
||||
// contains zero favorites
|
||||
f.addLeftJoin(`(SELECT performers_galleries.gallery_id as id FROM performers_galleries
|
||||
JOIN performers ON performers.id = performers_galleries.performer_id
|
||||
GROUP BY performers_galleries.gallery_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "galleries.id = nofaves.id")
|
||||
f.addWhere("performers_galleries.gallery_id IS NULL OR nofaves.id IS NOT NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) performerAgeCriterionHandler(performerAge *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performerAge != nil {
|
||||
f.addInnerJoin("performers_galleries", "", "galleries.id = performers_galleries.gallery_id")
|
||||
f.addInnerJoin("performers", "", "performers_galleries.performer_id = performers.id")
|
||||
|
||||
f.addWhere("galleries.date != '' AND performers.birthdate != ''")
|
||||
f.addWhere("galleries.date IS NOT NULL AND performers.birthdate IS NOT NULL")
|
||||
|
||||
ageCalc := "cast(strftime('%Y.%m%d', galleries.date) - strftime('%Y.%m%d', performers.birthdate) as int)"
|
||||
whereClause, args := getIntWhereClause(ageCalc, performerAge.Modifier, performerAge.Value, performerAge.Value2)
|
||||
f.addWhere(whereClause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *galleryFilterHandler) averageResolutionCriterionHandler(resolution *models.ResolutionCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if resolution != nil && resolution.Value.IsValid() {
|
||||
galleryRepository.images.join(f, "images_join", "galleries.id")
|
||||
f.addLeftJoin("images", "", "images_join.image_id = images.id")
|
||||
f.addLeftJoin("images_files", "", "images.id = images_files.image_id")
|
||||
f.addLeftJoin("image_files", "", "images_files.file_id = image_files.file_id")
|
||||
|
||||
min := resolution.Value.GetMinResolution()
|
||||
max := resolution.Value.GetMaxResolution()
|
||||
|
||||
const widthHeight = "avg(MIN(image_files.width, image_files.height))"
|
||||
|
||||
switch resolution.Modifier {
|
||||
case models.CriterionModifierEquals:
|
||||
f.addHaving(fmt.Sprintf("%s BETWEEN %d AND %d", widthHeight, min, max))
|
||||
case models.CriterionModifierNotEquals:
|
||||
f.addHaving(fmt.Sprintf("%s NOT BETWEEN %d AND %d", widthHeight, min, max))
|
||||
case models.CriterionModifierLessThan:
|
||||
f.addHaving(fmt.Sprintf("%s < %d", widthHeight, min))
|
||||
case models.CriterionModifierGreaterThan:
|
||||
f.addHaving(fmt.Sprintf("%s > %d", widthHeight, max))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1534,10 +1534,12 @@ func TestGalleryQueryPathOr(t *testing.T) {
|
||||
Value: gallery1Path,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
Or: &models.GalleryFilterType{
|
||||
Path: &models.StringCriterionInput{
|
||||
Value: gallery2Path,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
OperatorFilter: models.OperatorFilter[models.GalleryFilterType]{
|
||||
Or: &models.GalleryFilterType{
|
||||
Path: &models.StringCriterionInput{
|
||||
Value: gallery2Path,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1568,10 +1570,12 @@ func TestGalleryQueryPathAndRating(t *testing.T) {
|
||||
Value: galleryPath,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
And: &models.GalleryFilterType{
|
||||
Rating100: &models.IntCriterionInput{
|
||||
Value: *galleryRating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
OperatorFilter: models.OperatorFilter[models.GalleryFilterType]{
|
||||
And: &models.GalleryFilterType{
|
||||
Rating100: &models.IntCriterionInput{
|
||||
Value: *galleryRating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1609,8 +1613,10 @@ func TestGalleryQueryPathNotRating(t *testing.T) {
|
||||
|
||||
galleryFilter := models.GalleryFilterType{
|
||||
Path: &pathCriterion,
|
||||
Not: &models.GalleryFilterType{
|
||||
Rating100: &ratingCriterion,
|
||||
OperatorFilter: models.OperatorFilter[models.GalleryFilterType]{
|
||||
Not: &models.GalleryFilterType{
|
||||
Rating100: &ratingCriterion,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1641,8 +1647,10 @@ func TestGalleryIllegalQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
galleryFilter := &models.GalleryFilterType{
|
||||
And: &subFilter,
|
||||
Or: &subFilter,
|
||||
OperatorFilter: models.OperatorFilter[models.GalleryFilterType]{
|
||||
And: &subFilter,
|
||||
Or: &subFilter,
|
||||
},
|
||||
}
|
||||
|
||||
withTxn(func(ctx context.Context) error {
|
||||
|
||||
@@ -112,24 +112,87 @@ func (r *imageRowRecord) fromPartial(i models.ImagePartial) {
|
||||
r.setTimestamp("updated_at", i.UpdatedAt)
|
||||
}
|
||||
|
||||
type ImageStore struct {
|
||||
type imageRepositoryType struct {
|
||||
repository
|
||||
|
||||
tableMgr *table
|
||||
oCounterManager
|
||||
|
||||
fileStore *FileStore
|
||||
performers joinRepository
|
||||
galleries joinRepository
|
||||
tags joinRepository
|
||||
files filesRepository
|
||||
}
|
||||
|
||||
func NewImageStore(fileStore *FileStore) *ImageStore {
|
||||
return &ImageStore{
|
||||
func (r *imageRepositoryType) addImagesFilesTable(f *filterBuilder) {
|
||||
f.addLeftJoin(imagesFilesTable, "", "images_files.image_id = images.id")
|
||||
}
|
||||
|
||||
func (r *imageRepositoryType) addFilesTable(f *filterBuilder) {
|
||||
r.addImagesFilesTable(f)
|
||||
f.addLeftJoin(fileTable, "", "images_files.file_id = files.id")
|
||||
}
|
||||
|
||||
func (r *imageRepositoryType) addFoldersTable(f *filterBuilder) {
|
||||
r.addFilesTable(f)
|
||||
f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id")
|
||||
}
|
||||
|
||||
func (r *imageRepositoryType) addImageFilesTable(f *filterBuilder) {
|
||||
r.addImagesFilesTable(f)
|
||||
f.addLeftJoin(imageFileTable, "", "image_files.file_id = images_files.file_id")
|
||||
}
|
||||
|
||||
var (
|
||||
imageRepository = imageRepositoryType{
|
||||
repository: repository{
|
||||
tableName: imageTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
|
||||
performers: joinRepository{
|
||||
repository: repository{
|
||||
tableName: performersImagesTable,
|
||||
idColumn: imageIDColumn,
|
||||
},
|
||||
fkColumn: performerIDColumn,
|
||||
},
|
||||
|
||||
galleries: joinRepository{
|
||||
repository: repository{
|
||||
tableName: galleriesImagesTable,
|
||||
idColumn: imageIDColumn,
|
||||
},
|
||||
fkColumn: galleryIDColumn,
|
||||
},
|
||||
|
||||
files: filesRepository{
|
||||
repository: repository{
|
||||
tableName: imagesFilesTable,
|
||||
idColumn: imageIDColumn,
|
||||
},
|
||||
},
|
||||
|
||||
tags: joinRepository{
|
||||
repository: repository{
|
||||
tableName: imagesTagsTable,
|
||||
idColumn: imageIDColumn,
|
||||
},
|
||||
fkColumn: tagIDColumn,
|
||||
foreignTable: tagTable,
|
||||
orderBy: "tags.name ASC",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type ImageStore struct {
|
||||
tableMgr *table
|
||||
oCounterManager
|
||||
|
||||
repo *storeRepository
|
||||
}
|
||||
|
||||
func NewImageStore(r *storeRepository) *ImageStore {
|
||||
return &ImageStore{
|
||||
tableMgr: imageTableMgr,
|
||||
oCounterManager: oCounterManager{imageTableMgr},
|
||||
fileStore: fileStore,
|
||||
repo: r,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,13 +481,13 @@ func (qb *ImageStore) getMany(ctx context.Context, q *goqu.SelectDataset) ([]*mo
|
||||
}
|
||||
|
||||
func (qb *ImageStore) GetFiles(ctx context.Context, id int) ([]models.File, error) {
|
||||
fileIDs, err := qb.filesRepository().get(ctx, id)
|
||||
fileIDs, err := imageRepository.files.get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// use fileStore to load files
|
||||
files, err := qb.fileStore.Find(ctx, fileIDs...)
|
||||
files, err := qb.repo.File.Find(ctx, fileIDs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -434,7 +497,7 @@ func (qb *ImageStore) GetFiles(ctx context.Context, id int) ([]models.File, erro
|
||||
|
||||
func (qb *ImageStore) GetManyFileIDs(ctx context.Context, ids []int) ([][]models.FileID, error) {
|
||||
const primaryOnly = false
|
||||
return qb.filesRepository().getMany(ctx, ids, primaryOnly)
|
||||
return imageRepository.files.getMany(ctx, ids, primaryOnly)
|
||||
}
|
||||
|
||||
func (qb *ImageStore) FindByFileID(ctx context.Context, fileID models.FileID) ([]*models.Image, error) {
|
||||
@@ -642,110 +705,6 @@ func (qb *ImageStore) All(ctx context.Context) ([]*models.Image, error) {
|
||||
return qb.getMany(ctx, qb.selectDataset())
|
||||
}
|
||||
|
||||
func (qb *ImageStore) validateFilter(imageFilter *models.ImageFilterType) error {
|
||||
const and = "AND"
|
||||
const or = "OR"
|
||||
const not = "NOT"
|
||||
|
||||
if imageFilter.And != nil {
|
||||
if imageFilter.Or != nil {
|
||||
return illegalFilterCombination(and, or)
|
||||
}
|
||||
if imageFilter.Not != nil {
|
||||
return illegalFilterCombination(and, not)
|
||||
}
|
||||
|
||||
return qb.validateFilter(imageFilter.And)
|
||||
}
|
||||
|
||||
if imageFilter.Or != nil {
|
||||
if imageFilter.Not != nil {
|
||||
return illegalFilterCombination(or, not)
|
||||
}
|
||||
|
||||
return qb.validateFilter(imageFilter.Or)
|
||||
}
|
||||
|
||||
if imageFilter.Not != nil {
|
||||
return qb.validateFilter(imageFilter.Not)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageFilterType) *filterBuilder {
|
||||
query := &filterBuilder{}
|
||||
|
||||
if imageFilter.And != nil {
|
||||
query.and(qb.makeFilter(ctx, imageFilter.And))
|
||||
}
|
||||
if imageFilter.Or != nil {
|
||||
query.or(qb.makeFilter(ctx, imageFilter.Or))
|
||||
}
|
||||
if imageFilter.Not != nil {
|
||||
query.not(qb.makeFilter(ctx, imageFilter.Not))
|
||||
}
|
||||
|
||||
query.handleCriterion(ctx, intCriterionHandler(imageFilter.ID, "images.id", nil))
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if imageFilter.Checksum != nil {
|
||||
qb.addImagesFilesTable(f)
|
||||
f.addInnerJoin(fingerprintTable, "fingerprints_md5", "images_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
|
||||
}
|
||||
|
||||
stringCriterionHandler(imageFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
|
||||
}))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Title, "images.title"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Code, "images.code"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Details, "images.details"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Photographer, "images.photographer"))
|
||||
|
||||
query.handleCriterion(ctx, pathCriterionHandler(imageFilter.Path, "folders.path", "files.basename", qb.addFoldersTable))
|
||||
query.handleCriterion(ctx, imageFileCountCriterionHandler(qb, imageFilter.FileCount))
|
||||
query.handleCriterion(ctx, intCriterionHandler(imageFilter.Rating100, "images.rating", nil))
|
||||
query.handleCriterion(ctx, intCriterionHandler(imageFilter.OCounter, "images.o_counter", nil))
|
||||
query.handleCriterion(ctx, boolCriterionHandler(imageFilter.Organized, "images.organized", nil))
|
||||
query.handleCriterion(ctx, dateCriterionHandler(imageFilter.Date, "images.date"))
|
||||
query.handleCriterion(ctx, imageURLsCriterionHandler(imageFilter.URL))
|
||||
|
||||
query.handleCriterion(ctx, resolutionCriterionHandler(imageFilter.Resolution, "image_files.height", "image_files.width", qb.addImageFilesTable))
|
||||
query.handleCriterion(ctx, orientationCriterionHandler(imageFilter.Orientation, "image_files.height", "image_files.width", qb.addImageFilesTable))
|
||||
query.handleCriterion(ctx, imageIsMissingCriterionHandler(qb, imageFilter.IsMissing))
|
||||
|
||||
query.handleCriterion(ctx, imageTagsCriterionHandler(qb, imageFilter.Tags))
|
||||
query.handleCriterion(ctx, imageTagCountCriterionHandler(qb, imageFilter.TagCount))
|
||||
query.handleCriterion(ctx, imageGalleriesCriterionHandler(qb, imageFilter.Galleries))
|
||||
query.handleCriterion(ctx, imagePerformersCriterionHandler(qb, imageFilter.Performers))
|
||||
query.handleCriterion(ctx, imagePerformerCountCriterionHandler(qb, imageFilter.PerformerCount))
|
||||
query.handleCriterion(ctx, studioCriterionHandler(imageTable, imageFilter.Studios))
|
||||
query.handleCriterion(ctx, imagePerformerTagsCriterionHandler(qb, imageFilter.PerformerTags))
|
||||
query.handleCriterion(ctx, imagePerformerFavoriteCriterionHandler(imageFilter.PerformerFavorite))
|
||||
query.handleCriterion(ctx, imagePerformerAgeCriterionHandler(imageFilter.PerformerAge))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(imageFilter.CreatedAt, "images.created_at"))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(imageFilter.UpdatedAt, "images.updated_at"))
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (qb *ImageStore) addImagesFilesTable(f *filterBuilder) {
|
||||
f.addLeftJoin(imagesFilesTable, "", "images_files.image_id = images.id")
|
||||
}
|
||||
|
||||
func (qb *ImageStore) addFilesTable(f *filterBuilder) {
|
||||
qb.addImagesFilesTable(f)
|
||||
f.addLeftJoin(fileTable, "", "images_files.file_id = files.id")
|
||||
}
|
||||
|
||||
func (qb *ImageStore) addFoldersTable(f *filterBuilder) {
|
||||
qb.addFilesTable(f)
|
||||
f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id")
|
||||
}
|
||||
|
||||
func (qb *ImageStore) addImageFilesTable(f *filterBuilder) {
|
||||
qb.addImagesFilesTable(f)
|
||||
f.addLeftJoin(imageFileTable, "", "image_files.file_id = images_files.file_id")
|
||||
}
|
||||
|
||||
func (qb *ImageStore) makeQuery(ctx context.Context, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
|
||||
if imageFilter == nil {
|
||||
imageFilter = &models.ImageFilterType{}
|
||||
@@ -754,7 +713,7 @@ func (qb *ImageStore) makeQuery(ctx context.Context, imageFilter *models.ImageFi
|
||||
findFilter = &models.FindFilterType{}
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
query := imageRepository.newQuery()
|
||||
distinctIDs(&query, imageTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
@@ -782,10 +741,9 @@ func (qb *ImageStore) makeQuery(ctx context.Context, imageFilter *models.ImageFi
|
||||
query.parseQueryString(searchColumns, *q)
|
||||
}
|
||||
|
||||
if err := qb.validateFilter(imageFilter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter := qb.makeFilter(ctx, imageFilter)
|
||||
filter := filterBuilderFromHandler(ctx, &imageFilterHandler{
|
||||
imageFilter: imageFilter,
|
||||
})
|
||||
|
||||
if err := query.addFilter(filter); err != nil {
|
||||
return nil, err
|
||||
@@ -824,7 +782,7 @@ func (qb *ImageStore) queryGroupedFields(ctx context.Context, options models.Ima
|
||||
return models.NewImageQueryResult(qb), nil
|
||||
}
|
||||
|
||||
aggregateQuery := qb.newQuery()
|
||||
aggregateQuery := imageRepository.newQuery()
|
||||
|
||||
if options.Count {
|
||||
aggregateQuery.addColumn("COUNT(DISTINCT temp.id) as total")
|
||||
@@ -868,7 +826,7 @@ func (qb *ImageStore) queryGroupedFields(ctx context.Context, options models.Ima
|
||||
Megapixels null.Float
|
||||
Size null.Float
|
||||
}{}
|
||||
if err := qb.repository.queryStruct(ctx, aggregateQuery.toSQL(includeSortPagination), query.args, &out); err != nil {
|
||||
if err := imageRepository.queryStruct(ctx, aggregateQuery.toSQL(includeSortPagination), query.args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -888,171 +846,6 @@ func (qb *ImageStore) QueryCount(ctx context.Context, imageFilter *models.ImageF
|
||||
return query.executeCount(ctx)
|
||||
}
|
||||
|
||||
func imageFileCountCriterionHandler(qb *ImageStore, fileCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: imageTable,
|
||||
joinTable: imagesFilesTable,
|
||||
primaryFK: imageIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(fileCount)
|
||||
}
|
||||
|
||||
func imageIsMissingCriterionHandler(qb *ImageStore, isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "studio":
|
||||
f.addWhere("images.studio_id IS NULL")
|
||||
case "performers":
|
||||
qb.performersRepository().join(f, "performers_join", "images.id")
|
||||
f.addWhere("performers_join.image_id IS NULL")
|
||||
case "galleries":
|
||||
qb.galleriesRepository().join(f, "galleries_join", "images.id")
|
||||
f.addWhere("galleries_join.image_id IS NULL")
|
||||
case "tags":
|
||||
qb.tagsRepository().join(f, "tags_join", "images.id")
|
||||
f.addWhere("tags_join.image_id IS NULL")
|
||||
default:
|
||||
f.addWhere("(images." + *isMissing + " IS NULL OR TRIM(images." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func imageURLsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: imagesURLsTable,
|
||||
stringColumn: imageURLColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
imagesURLsTableMgr.join(f, "", "images.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(url)
|
||||
}
|
||||
|
||||
func (qb *ImageStore) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
|
||||
return multiCriterionHandlerBuilder{
|
||||
primaryTable: imageTable,
|
||||
foreignTable: foreignTable,
|
||||
joinTable: joinTable,
|
||||
primaryFK: imageIDColumn,
|
||||
foreignFK: foreignFK,
|
||||
addJoinsFunc: addJoinsFunc,
|
||||
}
|
||||
}
|
||||
|
||||
func imageTagsCriterionHandler(qb *ImageStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedHierarchicalMultiCriterionHandlerBuilder{
|
||||
tx: qb.tx,
|
||||
|
||||
primaryTable: imageTable,
|
||||
foreignTable: tagTable,
|
||||
foreignFK: "tag_id",
|
||||
|
||||
relationsTable: "tags_relations",
|
||||
joinAs: "image_tag",
|
||||
joinTable: imagesTagsTable,
|
||||
primaryFK: imageIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(tags)
|
||||
}
|
||||
|
||||
func imageTagCountCriterionHandler(qb *ImageStore, tagCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: imageTable,
|
||||
joinTable: imagesTagsTable,
|
||||
primaryFK: imageIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(tagCount)
|
||||
}
|
||||
|
||||
func imageGalleriesCriterionHandler(qb *ImageStore, galleries *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
if galleries.Modifier == models.CriterionModifierIncludes || galleries.Modifier == models.CriterionModifierIncludesAll {
|
||||
f.addInnerJoin(galleriesImagesTable, "", "galleries_images.image_id = images.id")
|
||||
f.addInnerJoin(galleryTable, "", "galleries_images.gallery_id = galleries.id")
|
||||
}
|
||||
}
|
||||
h := qb.getMultiCriterionHandlerBuilder(galleryTable, galleriesImagesTable, galleryIDColumn, addJoinsFunc)
|
||||
|
||||
return h.handler(galleries)
|
||||
}
|
||||
|
||||
func imagePerformersCriterionHandler(qb *ImageStore, performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedMultiCriterionHandlerBuilder{
|
||||
primaryTable: imageTable,
|
||||
joinTable: performersImagesTable,
|
||||
joinAs: "performers_join",
|
||||
primaryFK: imageIDColumn,
|
||||
foreignFK: performerIDColumn,
|
||||
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
qb.performersRepository().join(f, "performers_join", "images.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(performers)
|
||||
}
|
||||
|
||||
func imagePerformerCountCriterionHandler(qb *ImageStore, performerCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: imageTable,
|
||||
joinTable: performersImagesTable,
|
||||
primaryFK: imageIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(performerCount)
|
||||
}
|
||||
|
||||
func imagePerformerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performerfavorite != nil {
|
||||
f.addLeftJoin("performers_images", "", "images.id = performers_images.image_id")
|
||||
|
||||
if *performerfavorite {
|
||||
// contains at least one favorite
|
||||
f.addLeftJoin("performers", "", "performers.id = performers_images.performer_id")
|
||||
f.addWhere("performers.favorite = 1")
|
||||
} else {
|
||||
// contains zero favorites
|
||||
f.addLeftJoin(`(SELECT performers_images.image_id as id FROM performers_images
|
||||
JOIN performers ON performers.id = performers_images.performer_id
|
||||
GROUP BY performers_images.image_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "images.id = nofaves.id")
|
||||
f.addWhere("performers_images.image_id IS NULL OR nofaves.id IS NOT NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func imagePerformerAgeCriterionHandler(performerAge *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performerAge != nil {
|
||||
f.addInnerJoin("performers_images", "", "images.id = performers_images.image_id")
|
||||
f.addInnerJoin("performers", "", "performers_images.performer_id = performers.id")
|
||||
|
||||
f.addWhere("images.date != '' AND performers.birthdate != ''")
|
||||
f.addWhere("images.date IS NOT NULL AND performers.birthdate IS NOT NULL")
|
||||
|
||||
ageCalc := "cast(strftime('%Y.%m%d', images.date) - strftime('%Y.%m%d', performers.birthdate) as int)"
|
||||
whereClause, args := getIntWhereClause(ageCalc, performerAge.Modifier, performerAge.Value, performerAge.Value2)
|
||||
f.addWhere(whereClause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func imagePerformerTagsCriterionHandler(qb *ImageStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
|
||||
return &joinedPerformerTagsHandler{
|
||||
criterion: tags,
|
||||
primaryTable: imageTable,
|
||||
joinTable: performersImagesTable,
|
||||
joinPrimaryKey: imageIDColumn,
|
||||
}
|
||||
}
|
||||
|
||||
var imageSortOptions = sortOptions{
|
||||
"created_at",
|
||||
"date",
|
||||
@@ -1138,34 +931,13 @@ func (qb *ImageStore) setImageSortAndPagination(q *queryBuilder, findFilter *mod
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *ImageStore) galleriesRepository() *joinRepository {
|
||||
return &joinRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: galleriesImagesTable,
|
||||
idColumn: imageIDColumn,
|
||||
},
|
||||
fkColumn: galleryIDColumn,
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *ImageStore) filesRepository() *filesRepository {
|
||||
return &filesRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: imagesFilesTable,
|
||||
idColumn: imageIDColumn,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *ImageStore) AddFileID(ctx context.Context, id int, fileID models.FileID) error {
|
||||
const firstPrimary = false
|
||||
return imagesFilesTableMgr.insertJoins(ctx, id, firstPrimary, []models.FileID{fileID})
|
||||
}
|
||||
|
||||
func (qb *ImageStore) GetGalleryIDs(ctx context.Context, imageID int) ([]int, error) {
|
||||
return qb.galleriesRepository().getIDs(ctx, imageID)
|
||||
return imageRepository.galleries.getIDs(ctx, imageID)
|
||||
}
|
||||
|
||||
// func (qb *imageQueryBuilder) UpdateGalleries(ctx context.Context, imageID int, galleryIDs []int) error {
|
||||
@@ -1173,46 +945,22 @@ func (qb *ImageStore) GetGalleryIDs(ctx context.Context, imageID int) ([]int, er
|
||||
// return qb.galleriesRepository().replace(ctx, imageID, galleryIDs)
|
||||
// }
|
||||
|
||||
func (qb *ImageStore) performersRepository() *joinRepository {
|
||||
return &joinRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: performersImagesTable,
|
||||
idColumn: imageIDColumn,
|
||||
},
|
||||
fkColumn: performerIDColumn,
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *ImageStore) GetPerformerIDs(ctx context.Context, imageID int) ([]int, error) {
|
||||
return qb.performersRepository().getIDs(ctx, imageID)
|
||||
return imageRepository.performers.getIDs(ctx, imageID)
|
||||
}
|
||||
|
||||
func (qb *ImageStore) UpdatePerformers(ctx context.Context, imageID int, performerIDs []int) error {
|
||||
// Delete the existing joins and then create new ones
|
||||
return qb.performersRepository().replace(ctx, imageID, performerIDs)
|
||||
}
|
||||
|
||||
func (qb *ImageStore) tagsRepository() *joinRepository {
|
||||
return &joinRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: imagesTagsTable,
|
||||
idColumn: imageIDColumn,
|
||||
},
|
||||
fkColumn: tagIDColumn,
|
||||
foreignTable: tagTable,
|
||||
orderBy: "tags.name ASC",
|
||||
}
|
||||
return imageRepository.performers.replace(ctx, imageID, performerIDs)
|
||||
}
|
||||
|
||||
func (qb *ImageStore) GetTagIDs(ctx context.Context, imageID int) ([]int, error) {
|
||||
return qb.tagsRepository().getIDs(ctx, imageID)
|
||||
return imageRepository.tags.getIDs(ctx, imageID)
|
||||
}
|
||||
|
||||
func (qb *ImageStore) UpdateTags(ctx context.Context, imageID int, tagIDs []int) error {
|
||||
// Delete the existing joins and then create new ones
|
||||
return qb.tagsRepository().replace(ctx, imageID, tagIDs)
|
||||
return imageRepository.tags.replace(ctx, imageID, tagIDs)
|
||||
}
|
||||
|
||||
func (qb *ImageStore) GetURLs(ctx context.Context, imageID int) ([]string, error) {
|
||||
|
||||
290
pkg/sqlite/image_filter.go
Normal file
290
pkg/sqlite/image_filter.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type imageFilterHandler struct {
|
||||
imageFilter *models.ImageFilterType
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) validate() error {
|
||||
imageFilter := qb.imageFilter
|
||||
if imageFilter == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := validateFilterCombination(imageFilter.OperatorFilter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if subFilter := imageFilter.SubFilter(); subFilter != nil {
|
||||
sqb := &imageFilterHandler{imageFilter: subFilter}
|
||||
if err := sqb.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) handle(ctx context.Context, f *filterBuilder) {
|
||||
imageFilter := qb.imageFilter
|
||||
if imageFilter == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := qb.validate(); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
sf := imageFilter.SubFilter()
|
||||
if sf != nil {
|
||||
sub := &imageFilterHandler{sf}
|
||||
handleSubFilter(ctx, sub, f, imageFilter.OperatorFilter)
|
||||
}
|
||||
|
||||
f.handleCriterion(ctx, qb.criterionHandler())
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) criterionHandler() criterionHandler {
|
||||
imageFilter := qb.imageFilter
|
||||
return compoundHandler{
|
||||
intCriterionHandler(imageFilter.ID, "images.id", nil),
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if imageFilter.Checksum != nil {
|
||||
imageRepository.addImagesFilesTable(f)
|
||||
f.addInnerJoin(fingerprintTable, "fingerprints_md5", "images_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
|
||||
}
|
||||
|
||||
stringCriterionHandler(imageFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
|
||||
}),
|
||||
stringCriterionHandler(imageFilter.Title, "images.title"),
|
||||
stringCriterionHandler(imageFilter.Code, "images.code"),
|
||||
stringCriterionHandler(imageFilter.Details, "images.details"),
|
||||
stringCriterionHandler(imageFilter.Photographer, "images.photographer"),
|
||||
|
||||
pathCriterionHandler(imageFilter.Path, "folders.path", "files.basename", imageRepository.addFoldersTable),
|
||||
qb.fileCountCriterionHandler(imageFilter.FileCount),
|
||||
intCriterionHandler(imageFilter.Rating100, "images.rating", nil),
|
||||
intCriterionHandler(imageFilter.OCounter, "images.o_counter", nil),
|
||||
boolCriterionHandler(imageFilter.Organized, "images.organized", nil),
|
||||
&dateCriterionHandler{imageFilter.Date, "images.date", nil},
|
||||
qb.urlsCriterionHandler(imageFilter.URL),
|
||||
|
||||
resolutionCriterionHandler(imageFilter.Resolution, "image_files.height", "image_files.width", imageRepository.addImageFilesTable),
|
||||
orientationCriterionHandler(imageFilter.Orientation, "image_files.height", "image_files.width", imageRepository.addImageFilesTable),
|
||||
qb.missingCriterionHandler(imageFilter.IsMissing),
|
||||
|
||||
qb.tagsCriterionHandler(imageFilter.Tags),
|
||||
qb.tagCountCriterionHandler(imageFilter.TagCount),
|
||||
qb.galleriesCriterionHandler(imageFilter.Galleries),
|
||||
qb.performersCriterionHandler(imageFilter.Performers),
|
||||
qb.performerCountCriterionHandler(imageFilter.PerformerCount),
|
||||
studioCriterionHandler(imageTable, imageFilter.Studios),
|
||||
qb.performerTagsCriterionHandler(imageFilter.PerformerTags),
|
||||
qb.performerFavoriteCriterionHandler(imageFilter.PerformerFavorite),
|
||||
qb.performerAgeCriterionHandler(imageFilter.PerformerAge),
|
||||
×tampCriterionHandler{imageFilter.CreatedAt, "images.created_at", nil},
|
||||
×tampCriterionHandler{imageFilter.UpdatedAt, "images.updated_at", nil},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "galleries_images.gallery_id",
|
||||
relatedRepo: galleryRepository.repository,
|
||||
relatedHandler: &galleryFilterHandler{imageFilter.GalleriesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
imageRepository.galleries.innerJoin(f, "", "images.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "performers_join.performer_id",
|
||||
relatedRepo: performerRepository.repository,
|
||||
relatedHandler: &performerFilterHandler{imageFilter.PerformersFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
imageRepository.performers.innerJoin(f, "performers_join", "images.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "images.studio_id",
|
||||
relatedRepo: studioRepository.repository,
|
||||
relatedHandler: &studioFilterHandler{imageFilter.StudiosFilter},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "image_tag.tag_id",
|
||||
relatedRepo: tagRepository.repository,
|
||||
relatedHandler: &tagFilterHandler{imageFilter.TagsFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
imageRepository.tags.innerJoin(f, "image_tag", "images.id")
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) fileCountCriterionHandler(fileCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: imageTable,
|
||||
joinTable: imagesFilesTable,
|
||||
primaryFK: imageIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(fileCount)
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) missingCriterionHandler(isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "studio":
|
||||
f.addWhere("images.studio_id IS NULL")
|
||||
case "performers":
|
||||
imageRepository.performers.join(f, "performers_join", "images.id")
|
||||
f.addWhere("performers_join.image_id IS NULL")
|
||||
case "galleries":
|
||||
imageRepository.galleries.join(f, "galleries_join", "images.id")
|
||||
f.addWhere("galleries_join.image_id IS NULL")
|
||||
case "tags":
|
||||
imageRepository.tags.join(f, "tags_join", "images.id")
|
||||
f.addWhere("tags_join.image_id IS NULL")
|
||||
default:
|
||||
f.addWhere("(images." + *isMissing + " IS NULL OR TRIM(images." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) urlsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: imagesURLsTable,
|
||||
stringColumn: imageURLColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
imagesURLsTableMgr.join(f, "", "images.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(url)
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
|
||||
return multiCriterionHandlerBuilder{
|
||||
primaryTable: imageTable,
|
||||
foreignTable: foreignTable,
|
||||
joinTable: joinTable,
|
||||
primaryFK: imageIDColumn,
|
||||
foreignFK: foreignFK,
|
||||
addJoinsFunc: addJoinsFunc,
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) tagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedHierarchicalMultiCriterionHandlerBuilder{
|
||||
primaryTable: imageTable,
|
||||
foreignTable: tagTable,
|
||||
foreignFK: "tag_id",
|
||||
|
||||
relationsTable: "tags_relations",
|
||||
joinAs: "image_tag",
|
||||
joinTable: imagesTagsTable,
|
||||
primaryFK: imageIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(tags)
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) tagCountCriterionHandler(tagCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: imageTable,
|
||||
joinTable: imagesTagsTable,
|
||||
primaryFK: imageIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(tagCount)
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) galleriesCriterionHandler(galleries *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
if galleries.Modifier == models.CriterionModifierIncludes || galleries.Modifier == models.CriterionModifierIncludesAll {
|
||||
f.addInnerJoin(galleriesImagesTable, "", "galleries_images.image_id = images.id")
|
||||
f.addInnerJoin(galleryTable, "", "galleries_images.gallery_id = galleries.id")
|
||||
}
|
||||
}
|
||||
h := qb.getMultiCriterionHandlerBuilder(galleryTable, galleriesImagesTable, galleryIDColumn, addJoinsFunc)
|
||||
|
||||
return h.handler(galleries)
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) performersCriterionHandler(performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedMultiCriterionHandlerBuilder{
|
||||
primaryTable: imageTable,
|
||||
joinTable: performersImagesTable,
|
||||
joinAs: "performers_join",
|
||||
primaryFK: imageIDColumn,
|
||||
foreignFK: performerIDColumn,
|
||||
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
imageRepository.performers.join(f, "performers_join", "images.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(performers)
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) performerCountCriterionHandler(performerCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: imageTable,
|
||||
joinTable: performersImagesTable,
|
||||
primaryFK: imageIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(performerCount)
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) performerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performerfavorite != nil {
|
||||
f.addLeftJoin("performers_images", "", "images.id = performers_images.image_id")
|
||||
|
||||
if *performerfavorite {
|
||||
// contains at least one favorite
|
||||
f.addLeftJoin("performers", "", "performers.id = performers_images.performer_id")
|
||||
f.addWhere("performers.favorite = 1")
|
||||
} else {
|
||||
// contains zero favorites
|
||||
f.addLeftJoin(`(SELECT performers_images.image_id as id FROM performers_images
|
||||
JOIN performers ON performers.id = performers_images.performer_id
|
||||
GROUP BY performers_images.image_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "images.id = nofaves.id")
|
||||
f.addWhere("performers_images.image_id IS NULL OR nofaves.id IS NOT NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) performerAgeCriterionHandler(performerAge *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performerAge != nil {
|
||||
f.addInnerJoin("performers_images", "", "images.id = performers_images.image_id")
|
||||
f.addInnerJoin("performers", "", "performers_images.performer_id = performers.id")
|
||||
|
||||
f.addWhere("images.date != '' AND performers.birthdate != ''")
|
||||
f.addWhere("images.date IS NOT NULL AND performers.birthdate IS NOT NULL")
|
||||
|
||||
ageCalc := "cast(strftime('%Y.%m%d', images.date) - strftime('%Y.%m%d', performers.birthdate) as int)"
|
||||
whereClause, args := getIntWhereClause(ageCalc, performerAge.Modifier, performerAge.Value, performerAge.Value2)
|
||||
f.addWhere(whereClause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *imageFilterHandler) performerTagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandler {
|
||||
return &joinedPerformerTagsHandler{
|
||||
criterion: tags,
|
||||
primaryTable: imageTable,
|
||||
joinTable: performersImagesTable,
|
||||
joinPrimaryKey: imageIDColumn,
|
||||
}
|
||||
}
|
||||
@@ -1668,10 +1668,12 @@ func TestImageQueryPathOr(t *testing.T) {
|
||||
Value: image1Path,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
Or: &models.ImageFilterType{
|
||||
Path: &models.StringCriterionInput{
|
||||
Value: image2Path,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
OperatorFilter: models.OperatorFilter[models.ImageFilterType]{
|
||||
Or: &models.ImageFilterType{
|
||||
Path: &models.StringCriterionInput{
|
||||
Value: image2Path,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1702,10 +1704,12 @@ func TestImageQueryPathAndRating(t *testing.T) {
|
||||
Value: imagePath,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
And: &models.ImageFilterType{
|
||||
Rating100: &models.IntCriterionInput{
|
||||
Value: int(imageRating.Int64),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
OperatorFilter: models.OperatorFilter[models.ImageFilterType]{
|
||||
And: &models.ImageFilterType{
|
||||
Rating100: &models.IntCriterionInput{
|
||||
Value: int(imageRating.Int64),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1743,8 +1747,10 @@ func TestImageQueryPathNotRating(t *testing.T) {
|
||||
|
||||
imageFilter := models.ImageFilterType{
|
||||
Path: &pathCriterion,
|
||||
Not: &models.ImageFilterType{
|
||||
Rating100: &ratingCriterion,
|
||||
OperatorFilter: models.OperatorFilter[models.ImageFilterType]{
|
||||
Not: &models.ImageFilterType{
|
||||
Rating100: &ratingCriterion,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1775,8 +1781,10 @@ func TestImageIllegalQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
imageFilter := &models.ImageFilterType{
|
||||
And: &subFilter,
|
||||
Or: &subFilter,
|
||||
OperatorFilter: models.OperatorFilter[models.ImageFilterType]{
|
||||
And: &subFilter,
|
||||
Or: &subFilter,
|
||||
},
|
||||
}
|
||||
|
||||
withTxn(func(ctx context.Context) error {
|
||||
|
||||
@@ -96,8 +96,25 @@ func (r *movieRowRecord) fromPartial(o models.MoviePartial) {
|
||||
r.setTimestamp("updated_at", o.UpdatedAt)
|
||||
}
|
||||
|
||||
type MovieStore struct {
|
||||
type movieRepositoryType struct {
|
||||
repository
|
||||
scenes repository
|
||||
}
|
||||
|
||||
var (
|
||||
movieRepository = movieRepositoryType{
|
||||
repository: repository{
|
||||
tableName: movieTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
scenes: repository{
|
||||
tableName: moviesScenesTable,
|
||||
idColumn: movieIDColumn,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type MovieStore struct {
|
||||
blobJoinQueryBuilder
|
||||
|
||||
tableMgr *table
|
||||
@@ -105,10 +122,6 @@ type MovieStore struct {
|
||||
|
||||
func NewMovieStore(blobStore *BlobStore) *MovieStore {
|
||||
return &MovieStore{
|
||||
repository: repository{
|
||||
tableName: movieTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
blobJoinQueryBuilder: blobJoinQueryBuilder{
|
||||
blobStore: blobStore,
|
||||
joinTable: movieTable,
|
||||
@@ -180,7 +193,7 @@ func (qb *MovieStore) Destroy(ctx context.Context, id int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return qb.destroyExisting(ctx, []int{id})
|
||||
return movieRepository.destroyExisting(ctx, []int{id})
|
||||
}
|
||||
|
||||
// returns nil, nil if not found
|
||||
@@ -327,25 +340,6 @@ func (qb *MovieStore) All(ctx context.Context) ([]*models.Movie, error) {
|
||||
))
|
||||
}
|
||||
|
||||
func (qb *MovieStore) makeFilter(ctx context.Context, movieFilter *models.MovieFilterType) *filterBuilder {
|
||||
query := &filterBuilder{}
|
||||
|
||||
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Name, "movies.name"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Director, "movies.director"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Synopsis, "movies.synopsis"))
|
||||
query.handleCriterion(ctx, intCriterionHandler(movieFilter.Rating100, "movies.rating", nil))
|
||||
query.handleCriterion(ctx, floatIntCriterionHandler(movieFilter.Duration, "movies.duration", nil))
|
||||
query.handleCriterion(ctx, movieIsMissingCriterionHandler(qb, movieFilter.IsMissing))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.URL, "movies.url"))
|
||||
query.handleCriterion(ctx, studioCriterionHandler(movieTable, movieFilter.Studios))
|
||||
query.handleCriterion(ctx, moviePerformersCriterionHandler(qb, movieFilter.Performers))
|
||||
query.handleCriterion(ctx, dateCriterionHandler(movieFilter.Date, "movies.date"))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(movieFilter.CreatedAt, "movies.created_at"))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(movieFilter.UpdatedAt, "movies.updated_at"))
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (qb *MovieStore) makeQuery(ctx context.Context, movieFilter *models.MovieFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
|
||||
if findFilter == nil {
|
||||
findFilter = &models.FindFilterType{}
|
||||
@@ -354,7 +348,7 @@ func (qb *MovieStore) makeQuery(ctx context.Context, movieFilter *models.MovieFi
|
||||
movieFilter = &models.MovieFilterType{}
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
query := movieRepository.newQuery()
|
||||
distinctIDs(&query, movieTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
@@ -362,7 +356,9 @@ func (qb *MovieStore) makeQuery(ctx context.Context, movieFilter *models.MovieFi
|
||||
query.parseQueryString(searchColumns, *q)
|
||||
}
|
||||
|
||||
filter := qb.makeFilter(ctx, movieFilter)
|
||||
filter := filterBuilderFromHandler(ctx, &movieFilterHandler{
|
||||
movieFilter: movieFilter,
|
||||
})
|
||||
|
||||
if err := query.addFilter(filter); err != nil {
|
||||
return nil, err
|
||||
@@ -407,71 +403,6 @@ func (qb *MovieStore) QueryCount(ctx context.Context, movieFilter *models.MovieF
|
||||
return query.executeCount(ctx)
|
||||
}
|
||||
|
||||
func movieIsMissingCriterionHandler(qb *MovieStore, isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "front_image":
|
||||
f.addWhere("movies.front_image_blob IS NULL")
|
||||
case "back_image":
|
||||
f.addWhere("movies.back_image_blob IS NULL")
|
||||
case "scenes":
|
||||
f.addLeftJoin("movies_scenes", "", "movies_scenes.movie_id = movies.id")
|
||||
f.addWhere("movies_scenes.scene_id IS NULL")
|
||||
default:
|
||||
f.addWhere("(movies." + *isMissing + " IS NULL OR TRIM(movies." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func moviePerformersCriterionHandler(qb *MovieStore, performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performers != nil {
|
||||
if performers.Modifier == models.CriterionModifierIsNull || performers.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if performers.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
f.addLeftJoin("movies_scenes", "", "movies.id = movies_scenes.movie_id")
|
||||
f.addLeftJoin("performers_scenes", "", "movies_scenes.scene_id = performers_scenes.scene_id")
|
||||
|
||||
f.addWhere(fmt.Sprintf("performers_scenes.performer_id IS %s NULL", notClause))
|
||||
return
|
||||
}
|
||||
|
||||
if len(performers.Value) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var args []interface{}
|
||||
for _, arg := range performers.Value {
|
||||
args = append(args, arg)
|
||||
}
|
||||
|
||||
// Hack, can't apply args to join, nor inner join on a left join, so use CTE instead
|
||||
f.addWith(`movies_performers AS (
|
||||
SELECT movies_scenes.movie_id, performers_scenes.performer_id
|
||||
FROM movies_scenes
|
||||
INNER JOIN performers_scenes ON movies_scenes.scene_id = performers_scenes.scene_id
|
||||
WHERE performers_scenes.performer_id IN`+getInBinding(len(performers.Value))+`
|
||||
)`, args...)
|
||||
f.addLeftJoin("movies_performers", "", "movies.id = movies_performers.movie_id")
|
||||
|
||||
switch performers.Modifier {
|
||||
case models.CriterionModifierIncludes:
|
||||
f.addWhere("movies_performers.performer_id IS NOT NULL")
|
||||
case models.CriterionModifierIncludesAll:
|
||||
f.addWhere("movies_performers.performer_id IS NOT NULL")
|
||||
f.addHaving("COUNT(DISTINCT movies_performers.performer_id) = ?", len(performers.Value))
|
||||
case models.CriterionModifierExcludes:
|
||||
f.addWhere("movies_performers.performer_id IS NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var movieSortOptions = sortOptions{
|
||||
"created_at",
|
||||
"date",
|
||||
@@ -516,7 +447,7 @@ func (qb *MovieStore) getMovieSort(findFilter *models.FindFilterType) (string, e
|
||||
func (qb *MovieStore) queryMovies(ctx context.Context, query string, args []interface{}) ([]*models.Movie, error) {
|
||||
const single = false
|
||||
var ret []*models.Movie
|
||||
if err := qb.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
|
||||
if err := movieRepository.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
|
||||
var f movieRow
|
||||
if err := r.StructScan(&f); err != nil {
|
||||
return err
|
||||
@@ -586,7 +517,7 @@ INNER JOIN performers_scenes ON performers_scenes.scene_id = movies_scenes.scene
|
||||
WHERE performers_scenes.performer_id = ?
|
||||
`
|
||||
args := []interface{}{performerID}
|
||||
return qb.runCountQuery(ctx, query, args)
|
||||
return movieRepository.runCountQuery(ctx, query, args)
|
||||
}
|
||||
|
||||
func (qb *MovieStore) FindByStudioID(ctx context.Context, studioID int) ([]*models.Movie, error) {
|
||||
@@ -604,5 +535,5 @@ FROM movies
|
||||
WHERE movies.studio_id = ?
|
||||
`
|
||||
args := []interface{}{studioID}
|
||||
return qb.runCountQuery(ctx, query, args)
|
||||
return movieRepository.runCountQuery(ctx, query, args)
|
||||
}
|
||||
|
||||
150
pkg/sqlite/movies_filter.go
Normal file
150
pkg/sqlite/movies_filter.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type movieFilterHandler struct {
|
||||
movieFilter *models.MovieFilterType
|
||||
}
|
||||
|
||||
func (qb *movieFilterHandler) validate() error {
|
||||
movieFilter := qb.movieFilter
|
||||
if movieFilter == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := validateFilterCombination(movieFilter.OperatorFilter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if subFilter := movieFilter.SubFilter(); subFilter != nil {
|
||||
sqb := &movieFilterHandler{movieFilter: subFilter}
|
||||
if err := sqb.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *movieFilterHandler) handle(ctx context.Context, f *filterBuilder) {
|
||||
movieFilter := qb.movieFilter
|
||||
if movieFilter == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := qb.validate(); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
sf := movieFilter.SubFilter()
|
||||
if sf != nil {
|
||||
sub := &movieFilterHandler{sf}
|
||||
handleSubFilter(ctx, sub, f, movieFilter.OperatorFilter)
|
||||
}
|
||||
|
||||
f.handleCriterion(ctx, qb.criterionHandler())
|
||||
}
|
||||
|
||||
func (qb *movieFilterHandler) criterionHandler() criterionHandler {
|
||||
movieFilter := qb.movieFilter
|
||||
return compoundHandler{
|
||||
stringCriterionHandler(movieFilter.Name, "movies.name"),
|
||||
stringCriterionHandler(movieFilter.Director, "movies.director"),
|
||||
stringCriterionHandler(movieFilter.Synopsis, "movies.synopsis"),
|
||||
intCriterionHandler(movieFilter.Rating100, "movies.rating", nil),
|
||||
floatIntCriterionHandler(movieFilter.Duration, "movies.duration", nil),
|
||||
qb.missingCriterionHandler(movieFilter.IsMissing),
|
||||
stringCriterionHandler(movieFilter.URL, "movies.url"),
|
||||
studioCriterionHandler(movieTable, movieFilter.Studios),
|
||||
qb.performersCriterionHandler(movieFilter.Performers),
|
||||
&dateCriterionHandler{movieFilter.Date, "movies.date", nil},
|
||||
×tampCriterionHandler{movieFilter.CreatedAt, "movies.created_at", nil},
|
||||
×tampCriterionHandler{movieFilter.UpdatedAt, "movies.updated_at", nil},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "movies_scenes.scene_id",
|
||||
relatedRepo: sceneRepository.repository,
|
||||
relatedHandler: &sceneFilterHandler{movieFilter.ScenesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
movieRepository.scenes.innerJoin(f, "", "movies.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "movies.studio_id",
|
||||
relatedRepo: studioRepository.repository,
|
||||
relatedHandler: &studioFilterHandler{movieFilter.StudiosFilter},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *movieFilterHandler) missingCriterionHandler(isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "front_image":
|
||||
f.addWhere("movies.front_image_blob IS NULL")
|
||||
case "back_image":
|
||||
f.addWhere("movies.back_image_blob IS NULL")
|
||||
case "scenes":
|
||||
f.addLeftJoin("movies_scenes", "", "movies_scenes.movie_id = movies.id")
|
||||
f.addWhere("movies_scenes.scene_id IS NULL")
|
||||
default:
|
||||
f.addWhere("(movies." + *isMissing + " IS NULL OR TRIM(movies." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *movieFilterHandler) performersCriterionHandler(performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performers != nil {
|
||||
if performers.Modifier == models.CriterionModifierIsNull || performers.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if performers.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
f.addLeftJoin("movies_scenes", "", "movies.id = movies_scenes.movie_id")
|
||||
f.addLeftJoin("performers_scenes", "", "movies_scenes.scene_id = performers_scenes.scene_id")
|
||||
|
||||
f.addWhere(fmt.Sprintf("performers_scenes.performer_id IS %s NULL", notClause))
|
||||
return
|
||||
}
|
||||
|
||||
if len(performers.Value) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var args []interface{}
|
||||
for _, arg := range performers.Value {
|
||||
args = append(args, arg)
|
||||
}
|
||||
|
||||
// Hack, can't apply args to join, nor inner join on a left join, so use CTE instead
|
||||
f.addWith(`movies_performers AS (
|
||||
SELECT movies_scenes.movie_id, performers_scenes.performer_id
|
||||
FROM movies_scenes
|
||||
INNER JOIN performers_scenes ON movies_scenes.scene_id = performers_scenes.scene_id
|
||||
WHERE performers_scenes.performer_id IN`+getInBinding(len(performers.Value))+`
|
||||
)`, args...)
|
||||
f.addLeftJoin("movies_performers", "", "movies.id = movies_performers.movie_id")
|
||||
|
||||
switch performers.Modifier {
|
||||
case models.CriterionModifierIncludes:
|
||||
f.addWhere("movies_performers.performer_id IS NOT NULL")
|
||||
case models.CriterionModifierIncludesAll:
|
||||
f.addWhere("movies_performers.performer_id IS NOT NULL")
|
||||
f.addHaving("COUNT(DISTINCT movies_performers.performer_id) = ?", len(performers.Value))
|
||||
case models.CriterionModifierExcludes:
|
||||
f.addWhere("movies_performers.performer_id IS NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/doug-martin/goqu/v9/exp"
|
||||
@@ -176,8 +174,66 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) {
|
||||
r.setBool("ignore_auto_tag", o.IgnoreAutoTag)
|
||||
}
|
||||
|
||||
type PerformerStore struct {
|
||||
type performerRepositoryType struct {
|
||||
repository
|
||||
|
||||
tags joinRepository
|
||||
stashIDs stashIDRepository
|
||||
|
||||
scenes joinRepository
|
||||
images joinRepository
|
||||
galleries joinRepository
|
||||
}
|
||||
|
||||
var (
|
||||
performerRepository = performerRepositoryType{
|
||||
repository: repository{
|
||||
tableName: performerTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
tags: joinRepository{
|
||||
repository: repository{
|
||||
tableName: performersTagsTable,
|
||||
idColumn: performerIDColumn,
|
||||
},
|
||||
fkColumn: tagIDColumn,
|
||||
foreignTable: tagTable,
|
||||
orderBy: "tags.name ASC",
|
||||
},
|
||||
stashIDs: stashIDRepository{
|
||||
repository{
|
||||
tableName: "performer_stash_ids",
|
||||
idColumn: performerIDColumn,
|
||||
},
|
||||
},
|
||||
scenes: joinRepository{
|
||||
repository: repository{
|
||||
tableName: performersScenesTable,
|
||||
idColumn: performerIDColumn,
|
||||
},
|
||||
fkColumn: sceneIDColumn,
|
||||
foreignTable: sceneTable,
|
||||
},
|
||||
images: joinRepository{
|
||||
repository: repository{
|
||||
tableName: performersImagesTable,
|
||||
idColumn: performerIDColumn,
|
||||
},
|
||||
fkColumn: imageIDColumn,
|
||||
foreignTable: imageTable,
|
||||
},
|
||||
galleries: joinRepository{
|
||||
repository: repository{
|
||||
tableName: performersGalleriesTable,
|
||||
idColumn: performerIDColumn,
|
||||
},
|
||||
fkColumn: galleryIDColumn,
|
||||
foreignTable: galleryTable,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type PerformerStore struct {
|
||||
blobJoinQueryBuilder
|
||||
|
||||
tableMgr *table
|
||||
@@ -185,10 +241,6 @@ type PerformerStore struct {
|
||||
|
||||
func NewPerformerStore(blobStore *BlobStore) *PerformerStore {
|
||||
return &PerformerStore{
|
||||
repository: repository{
|
||||
tableName: performerTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
blobJoinQueryBuilder: blobJoinQueryBuilder{
|
||||
blobStore: blobStore,
|
||||
joinTable: performerTable,
|
||||
@@ -312,7 +364,7 @@ func (qb *PerformerStore) Destroy(ctx context.Context, id int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return qb.destroyExisting(ctx, []int{id})
|
||||
return performerRepository.destroyExisting(ctx, []int{id})
|
||||
}
|
||||
|
||||
// returns nil, nil if not found
|
||||
@@ -525,161 +577,6 @@ func (qb *PerformerStore) QueryForAutoTag(ctx context.Context, words []string) (
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) validateFilter(filter *models.PerformerFilterType) error {
|
||||
const and = "AND"
|
||||
const or = "OR"
|
||||
const not = "NOT"
|
||||
|
||||
if filter.And != nil {
|
||||
if filter.Or != nil {
|
||||
return illegalFilterCombination(and, or)
|
||||
}
|
||||
if filter.Not != nil {
|
||||
return illegalFilterCombination(and, not)
|
||||
}
|
||||
|
||||
return qb.validateFilter(filter.And)
|
||||
}
|
||||
|
||||
if filter.Or != nil {
|
||||
if filter.Not != nil {
|
||||
return illegalFilterCombination(or, not)
|
||||
}
|
||||
|
||||
return qb.validateFilter(filter.Or)
|
||||
}
|
||||
|
||||
if filter.Not != nil {
|
||||
return qb.validateFilter(filter.Not)
|
||||
}
|
||||
|
||||
// if legacy height filter used, ensure only supported modifiers are used
|
||||
if filter.Height != nil {
|
||||
// treat as an int filter
|
||||
intCrit := &models.IntCriterionInput{
|
||||
Modifier: filter.Height.Modifier,
|
||||
}
|
||||
if !intCrit.ValidModifier() {
|
||||
return fmt.Errorf("invalid height modifier: %s", filter.Height.Modifier)
|
||||
}
|
||||
|
||||
// ensure value is a valid number
|
||||
if _, err := strconv.Atoi(filter.Height.Value); err != nil {
|
||||
return fmt.Errorf("invalid height value: %s", filter.Height.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.PerformerFilterType) *filterBuilder {
|
||||
query := &filterBuilder{}
|
||||
|
||||
if filter.And != nil {
|
||||
query.and(qb.makeFilter(ctx, filter.And))
|
||||
}
|
||||
if filter.Or != nil {
|
||||
query.or(qb.makeFilter(ctx, filter.Or))
|
||||
}
|
||||
if filter.Not != nil {
|
||||
query.not(qb.makeFilter(ctx, filter.Not))
|
||||
}
|
||||
|
||||
const tableName = performerTable
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Name, tableName+".name"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Disambiguation, tableName+".disambiguation"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Details, tableName+".details"))
|
||||
|
||||
query.handleCriterion(ctx, boolCriterionHandler(filter.FilterFavorites, tableName+".favorite", nil))
|
||||
query.handleCriterion(ctx, boolCriterionHandler(filter.IgnoreAutoTag, tableName+".ignore_auto_tag", nil))
|
||||
|
||||
query.handleCriterion(ctx, yearFilterCriterionHandler(filter.BirthYear, tableName+".birthdate"))
|
||||
query.handleCriterion(ctx, yearFilterCriterionHandler(filter.DeathYear, tableName+".death_date"))
|
||||
|
||||
query.handleCriterion(ctx, performerAgeFilterCriterionHandler(filter.Age))
|
||||
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if gender := filter.Gender; gender != nil {
|
||||
genderCopy := *gender
|
||||
if genderCopy.Value.IsValid() && len(genderCopy.ValueList) == 0 {
|
||||
genderCopy.ValueList = []models.GenderEnum{genderCopy.Value}
|
||||
}
|
||||
|
||||
v := utils.StringerSliceToStringSlice(genderCopy.ValueList)
|
||||
enumCriterionHandler(genderCopy.Modifier, v, tableName+".gender")(ctx, f)
|
||||
}
|
||||
}))
|
||||
|
||||
query.handleCriterion(ctx, performerIsMissingCriterionHandler(qb, filter.IsMissing))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Ethnicity, tableName+".ethnicity"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Country, tableName+".country"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.EyeColor, tableName+".eye_color"))
|
||||
|
||||
// special handler for legacy height filter
|
||||
heightCmCrit := filter.HeightCm
|
||||
if heightCmCrit == nil && filter.Height != nil {
|
||||
heightCm, _ := strconv.Atoi(filter.Height.Value) // already validated
|
||||
heightCmCrit = &models.IntCriterionInput{
|
||||
Value: heightCm,
|
||||
Modifier: filter.Height.Modifier,
|
||||
}
|
||||
}
|
||||
|
||||
query.handleCriterion(ctx, intCriterionHandler(heightCmCrit, tableName+".height", nil))
|
||||
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Measurements, tableName+".measurements"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.FakeTits, tableName+".fake_tits"))
|
||||
query.handleCriterion(ctx, floatCriterionHandler(filter.PenisLength, tableName+".penis_length", nil))
|
||||
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if circumcised := filter.Circumcised; circumcised != nil {
|
||||
v := utils.StringerSliceToStringSlice(circumcised.Value)
|
||||
enumCriterionHandler(circumcised.Modifier, v, tableName+".circumcised")(ctx, f)
|
||||
}
|
||||
}))
|
||||
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.CareerLength, tableName+".career_length"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Tattoos, tableName+".tattoos"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Piercings, tableName+".piercings"))
|
||||
query.handleCriterion(ctx, intCriterionHandler(filter.Rating100, tableName+".rating", nil))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.HairColor, tableName+".hair_color"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.URL, tableName+".url"))
|
||||
query.handleCriterion(ctx, intCriterionHandler(filter.Weight, tableName+".weight", nil))
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if filter.StashID != nil {
|
||||
qb.stashIDRepository().join(f, "performer_stash_ids", "performers.id")
|
||||
stringCriterionHandler(filter.StashID, "performer_stash_ids.stash_id")(ctx, f)
|
||||
}
|
||||
}))
|
||||
query.handleCriterion(ctx, &stashIDCriterionHandler{
|
||||
c: filter.StashIDEndpoint,
|
||||
stashIDRepository: qb.stashIDRepository(),
|
||||
stashIDTableAs: "performer_stash_ids",
|
||||
parentIDCol: "performers.id",
|
||||
})
|
||||
|
||||
query.handleCriterion(ctx, performerAliasCriterionHandler(qb, filter.Aliases))
|
||||
|
||||
query.handleCriterion(ctx, performerTagsCriterionHandler(qb, filter.Tags))
|
||||
|
||||
query.handleCriterion(ctx, performerStudiosCriterionHandler(qb, filter.Studios))
|
||||
|
||||
query.handleCriterion(ctx, performerAppearsWithCriterionHandler(qb, filter.Performers))
|
||||
|
||||
query.handleCriterion(ctx, performerTagCountCriterionHandler(qb, filter.TagCount))
|
||||
query.handleCriterion(ctx, performerSceneCountCriterionHandler(qb, filter.SceneCount))
|
||||
query.handleCriterion(ctx, performerImageCountCriterionHandler(qb, filter.ImageCount))
|
||||
query.handleCriterion(ctx, performerGalleryCountCriterionHandler(qb, filter.GalleryCount))
|
||||
query.handleCriterion(ctx, performerPlayCounterCriterionHandler(qb, filter.PlayCount))
|
||||
query.handleCriterion(ctx, performerOCounterCriterionHandler(qb, filter.OCounter))
|
||||
query.handleCriterion(ctx, dateCriterionHandler(filter.Birthdate, tableName+".birthdate"))
|
||||
query.handleCriterion(ctx, dateCriterionHandler(filter.DeathDate, tableName+".death_date"))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(filter.CreatedAt, tableName+".created_at"))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(filter.UpdatedAt, tableName+".updated_at"))
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) makeQuery(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
|
||||
if performerFilter == nil {
|
||||
performerFilter = &models.PerformerFilterType{}
|
||||
@@ -688,7 +585,7 @@ func (qb *PerformerStore) makeQuery(ctx context.Context, performerFilter *models
|
||||
findFilter = &models.FindFilterType{}
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
query := performerRepository.newQuery()
|
||||
distinctIDs(&query, performerTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
@@ -697,10 +594,9 @@ func (qb *PerformerStore) makeQuery(ctx context.Context, performerFilter *models
|
||||
query.parseQueryString(searchColumns, *q)
|
||||
}
|
||||
|
||||
if err := qb.validateFilter(performerFilter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter := qb.makeFilter(ctx, performerFilter)
|
||||
filter := filterBuilderFromHandler(ctx, &performerFilterHandler{
|
||||
performerFilter: performerFilter,
|
||||
})
|
||||
|
||||
if err := query.addFilter(filter); err != nil {
|
||||
return nil, err
|
||||
@@ -744,165 +640,16 @@ func (qb *PerformerStore) QueryCount(ctx context.Context, performerFilter *model
|
||||
return query.executeCount(ctx)
|
||||
}
|
||||
|
||||
// TODO - we need to provide a whitelist of possible values
|
||||
func performerIsMissingCriterionHandler(qb *PerformerStore, isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "scenes": // Deprecated: use `scene_count == 0` filter instead
|
||||
f.addLeftJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id")
|
||||
f.addWhere("scenes_join.scene_id IS NULL")
|
||||
case "image":
|
||||
f.addWhere("performers.image_blob IS NULL")
|
||||
case "stash_id":
|
||||
performersStashIDsTableMgr.join(f, "performer_stash_ids", "performers.id")
|
||||
f.addWhere("performer_stash_ids.performer_id IS NULL")
|
||||
case "aliases":
|
||||
performersAliasesTableMgr.join(f, "", "performers.id")
|
||||
f.addWhere("performer_aliases.alias IS NULL")
|
||||
default:
|
||||
f.addWhere("(performers." + *isMissing + " IS NULL OR TRIM(performers." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
func (qb *PerformerStore) sortByOCounter(direction string) string {
|
||||
// need to sum the o_counter from scenes and images
|
||||
return " ORDER BY (" + selectPerformerOCountSQL + ") " + direction
|
||||
}
|
||||
|
||||
func yearFilterCriterionHandler(year *models.IntCriterionInput, col string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if year != nil && year.Modifier.IsValid() {
|
||||
clause, args := getIntCriterionWhereClause("cast(strftime('%Y', "+col+") as int)", *year)
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
func (qb *PerformerStore) sortByPlayCount(direction string) string {
|
||||
// need to sum the o_counter from scenes and images
|
||||
return " ORDER BY (" + selectPerformerPlayCountSQL + ") " + direction
|
||||
}
|
||||
|
||||
func performerAgeFilterCriterionHandler(age *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if age != nil && age.Modifier.IsValid() {
|
||||
clause, args := getIntCriterionWhereClause(
|
||||
"cast(IFNULL(strftime('%Y.%m%d', performers.death_date), strftime('%Y.%m%d', 'now')) - strftime('%Y.%m%d', performers.birthdate) as int)",
|
||||
*age,
|
||||
)
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func performerAliasCriterionHandler(qb *PerformerStore, alias *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: performersAliasesTable,
|
||||
stringColumn: performerAliasColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
performersAliasesTableMgr.join(f, "", "performers.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(alias)
|
||||
}
|
||||
|
||||
func performerTagsCriterionHandler(qb *PerformerStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedHierarchicalMultiCriterionHandlerBuilder{
|
||||
tx: qb.tx,
|
||||
|
||||
primaryTable: performerTable,
|
||||
foreignTable: tagTable,
|
||||
foreignFK: "tag_id",
|
||||
|
||||
relationsTable: "tags_relations",
|
||||
joinAs: "image_tag",
|
||||
joinTable: performersTagsTable,
|
||||
primaryFK: performerIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(tags)
|
||||
}
|
||||
|
||||
func performerTagCountCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: performerTable,
|
||||
joinTable: performersTagsTable,
|
||||
primaryFK: performerIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(count)
|
||||
}
|
||||
|
||||
func performerSceneCountCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: performerTable,
|
||||
joinTable: performersScenesTable,
|
||||
primaryFK: performerIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(count)
|
||||
}
|
||||
|
||||
func performerImageCountCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: performerTable,
|
||||
joinTable: performersImagesTable,
|
||||
primaryFK: performerIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(count)
|
||||
}
|
||||
|
||||
func performerGalleryCountCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: performerTable,
|
||||
joinTable: performersGalleriesTable,
|
||||
primaryFK: performerIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(count)
|
||||
}
|
||||
|
||||
// used for sorting and filtering on performer o-count
|
||||
var selectPerformerOCountSQL = utils.StrFormat(
|
||||
"SELECT SUM(o_counter) "+
|
||||
"FROM ("+
|
||||
"SELECT SUM(o_counter) as o_counter from {performers_images} s "+
|
||||
"LEFT JOIN {images} ON {images}.id = s.{images_id} "+
|
||||
"WHERE s.{performer_id} = {performers}.id "+
|
||||
"UNION ALL "+
|
||||
"SELECT COUNT({scenes_o_dates}.{o_date}) as o_counter from {performers_scenes} s "+
|
||||
"LEFT JOIN {scenes} ON {scenes}.id = s.{scene_id} "+
|
||||
"LEFT JOIN {scenes_o_dates} ON {scenes_o_dates}.{scene_id} = {scenes}.id "+
|
||||
"WHERE s.{performer_id} = {performers}.id "+
|
||||
")",
|
||||
map[string]interface{}{
|
||||
"performers_images": performersImagesTable,
|
||||
"images": imageTable,
|
||||
"performer_id": performerIDColumn,
|
||||
"images_id": imageIDColumn,
|
||||
"performers": performerTable,
|
||||
"performers_scenes": performersScenesTable,
|
||||
"scenes": sceneTable,
|
||||
"scene_id": sceneIDColumn,
|
||||
"scenes_o_dates": scenesODatesTable,
|
||||
"o_date": sceneODateColumn,
|
||||
},
|
||||
)
|
||||
|
||||
// used for sorting and filtering play count on performer view count
|
||||
var selectPerformerPlayCountSQL = utils.StrFormat(
|
||||
"SELECT COUNT(DISTINCT {view_date}) FROM ("+
|
||||
"SELECT {view_date} FROM {performers_scenes} s "+
|
||||
"LEFT JOIN {scenes} ON {scenes}.id = s.{scene_id} "+
|
||||
"LEFT JOIN {scenes_view_dates} ON {scenes_view_dates}.{scene_id} = {scenes}.id "+
|
||||
"WHERE s.{performer_id} = {performers}.id"+
|
||||
")",
|
||||
map[string]interface{}{
|
||||
"performer_id": performerIDColumn,
|
||||
"performers": performerTable,
|
||||
"performers_scenes": performersScenesTable,
|
||||
"scenes": sceneTable,
|
||||
"scene_id": sceneIDColumn,
|
||||
"scenes_view_dates": scenesViewDatesTable,
|
||||
"view_date": sceneViewDateColumn,
|
||||
},
|
||||
)
|
||||
|
||||
// used for sorting on performer last o_date
|
||||
var selectPerformerLastOAtSQL = utils.StrFormat(
|
||||
"SELECT MAX(o_date) FROM ("+
|
||||
@@ -922,6 +669,11 @@ var selectPerformerLastOAtSQL = utils.StrFormat(
|
||||
},
|
||||
)
|
||||
|
||||
func (qb *PerformerStore) sortByLastOAt(direction string) string {
|
||||
// need to get the o_dates from scenes
|
||||
return " ORDER BY (" + selectPerformerLastOAtSQL + ") " + direction
|
||||
}
|
||||
|
||||
// used for sorting on performer last view_date
|
||||
var selectPerformerLastPlayedAtSQL = utils.StrFormat(
|
||||
"SELECT MAX(view_date) FROM ("+
|
||||
@@ -941,182 +693,6 @@ var selectPerformerLastPlayedAtSQL = utils.StrFormat(
|
||||
},
|
||||
)
|
||||
|
||||
func performerOCounterCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if count == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lhs := "(" + selectPerformerOCountSQL + ")"
|
||||
clause, args := getIntCriterionWhereClause(lhs, *count)
|
||||
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func performerPlayCounterCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if count == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lhs := "(" + selectPerformerPlayCountSQL + ")"
|
||||
clause, args := getIntCriterionWhereClause(lhs, *count)
|
||||
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func performerStudiosCriterionHandler(qb *PerformerStore, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if studios != nil {
|
||||
formatMaps := []utils.StrFormatMap{
|
||||
{
|
||||
"primaryTable": sceneTable,
|
||||
"joinTable": performersScenesTable,
|
||||
"primaryFK": sceneIDColumn,
|
||||
},
|
||||
{
|
||||
"primaryTable": imageTable,
|
||||
"joinTable": performersImagesTable,
|
||||
"primaryFK": imageIDColumn,
|
||||
},
|
||||
{
|
||||
"primaryTable": galleryTable,
|
||||
"joinTable": performersGalleriesTable,
|
||||
"primaryFK": galleryIDColumn,
|
||||
},
|
||||
}
|
||||
|
||||
if studios.Modifier == models.CriterionModifierIsNull || studios.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if studios.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
var conditions []string
|
||||
for _, c := range formatMaps {
|
||||
f.addLeftJoin(c["joinTable"].(string), "", fmt.Sprintf("%s.performer_id = performers.id", c["joinTable"]))
|
||||
f.addLeftJoin(c["primaryTable"].(string), "", fmt.Sprintf("%s.%s = %s.id", c["joinTable"], c["primaryFK"], c["primaryTable"]))
|
||||
|
||||
conditions = append(conditions, fmt.Sprintf("%s.studio_id IS NULL", c["primaryTable"]))
|
||||
}
|
||||
|
||||
f.addWhere(fmt.Sprintf("%s (%s)", notClause, strings.Join(conditions, " AND ")))
|
||||
return
|
||||
}
|
||||
|
||||
if len(studios.Value) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var clauseCondition string
|
||||
|
||||
switch studios.Modifier {
|
||||
case models.CriterionModifierIncludes:
|
||||
// return performers who appear in scenes/images/galleries with any of the given studios
|
||||
clauseCondition = "NOT"
|
||||
case models.CriterionModifierExcludes:
|
||||
// exclude performers who appear in scenes/images/galleries with any of the given studios
|
||||
clauseCondition = ""
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
const derivedPerformerStudioTable = "performer_studio"
|
||||
valuesClause, err := getHierarchicalValues(ctx, qb.tx, studios.Value, studioTable, "", "parent_id", "child_id", studios.Depth)
|
||||
if err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
f.addWith("studio(root_id, item_id) AS (" + valuesClause + ")")
|
||||
|
||||
templStr := `SELECT performer_id FROM {primaryTable}
|
||||
INNER JOIN {joinTable} ON {primaryTable}.id = {joinTable}.{primaryFK}
|
||||
INNER JOIN studio ON {primaryTable}.studio_id = studio.item_id`
|
||||
|
||||
var unions []string
|
||||
for _, c := range formatMaps {
|
||||
unions = append(unions, utils.StrFormat(templStr, c))
|
||||
}
|
||||
|
||||
f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerStudioTable, strings.Join(unions, " UNION ")))
|
||||
|
||||
f.addLeftJoin(derivedPerformerStudioTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerStudioTable))
|
||||
f.addWhere(fmt.Sprintf("%s.performer_id IS %s NULL", derivedPerformerStudioTable, clauseCondition))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func performerAppearsWithCriterionHandler(qb *PerformerStore, performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performers != nil {
|
||||
formatMaps := []utils.StrFormatMap{
|
||||
{
|
||||
"primaryTable": performersScenesTable,
|
||||
"joinTable": performersScenesTable,
|
||||
"primaryFK": sceneIDColumn,
|
||||
},
|
||||
{
|
||||
"primaryTable": performersImagesTable,
|
||||
"joinTable": performersImagesTable,
|
||||
"primaryFK": imageIDColumn,
|
||||
},
|
||||
{
|
||||
"primaryTable": performersGalleriesTable,
|
||||
"joinTable": performersGalleriesTable,
|
||||
"primaryFK": galleryIDColumn,
|
||||
},
|
||||
}
|
||||
|
||||
if len(performers.Value) == '0' {
|
||||
return
|
||||
}
|
||||
|
||||
const derivedPerformerPerformersTable = "performer_performers"
|
||||
|
||||
valuesClause := strings.Join(performers.Value, "),(")
|
||||
|
||||
f.addWith("performer(id) AS (VALUES(" + valuesClause + "))")
|
||||
|
||||
templStr := `SELECT {primaryTable}2.performer_id FROM {primaryTable}
|
||||
INNER JOIN {primaryTable} AS {primaryTable}2 ON {primaryTable}.{primaryFK} = {primaryTable}2.{primaryFK}
|
||||
INNER JOIN performer ON {primaryTable}.performer_id = performer.id
|
||||
WHERE {primaryTable}2.performer_id != performer.id`
|
||||
|
||||
if performers.Modifier == models.CriterionModifierIncludesAll && len(performers.Value) > 1 {
|
||||
templStr += `
|
||||
GROUP BY {primaryTable}2.performer_id
|
||||
HAVING(count(distinct {primaryTable}.performer_id) IS ` + strconv.Itoa(len(performers.Value)) + `)`
|
||||
}
|
||||
|
||||
var unions []string
|
||||
for _, c := range formatMaps {
|
||||
unions = append(unions, utils.StrFormat(templStr, c))
|
||||
}
|
||||
|
||||
f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerPerformersTable, strings.Join(unions, " UNION ")))
|
||||
|
||||
f.addInnerJoin(derivedPerformerPerformersTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerPerformersTable))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) sortByOCounter(direction string) string {
|
||||
// need to sum the o_counter from scenes and images
|
||||
return " ORDER BY (" + selectPerformerOCountSQL + ") " + direction
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) sortByPlayCount(direction string) string {
|
||||
// need to sum the o_counter from scenes and images
|
||||
return " ORDER BY (" + selectPerformerPlayCountSQL + ") " + direction
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) sortByLastOAt(direction string) string {
|
||||
// need to get the o_dates from scenes
|
||||
return " ORDER BY (" + selectPerformerLastOAtSQL + ") " + direction
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) sortByLastPlayedAt(direction string) string {
|
||||
// need to get the view_dates from scenes
|
||||
return " ORDER BY (" + selectPerformerLastPlayedAtSQL + ") " + direction
|
||||
@@ -1185,21 +761,8 @@ func (qb *PerformerStore) getPerformerSort(findFilter *models.FindFilterType) (s
|
||||
return sortQuery, nil
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) tagsRepository() *joinRepository {
|
||||
return &joinRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: performersTagsTable,
|
||||
idColumn: performerIDColumn,
|
||||
},
|
||||
fkColumn: tagIDColumn,
|
||||
foreignTable: tagTable,
|
||||
orderBy: "tags.name ASC",
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) GetTagIDs(ctx context.Context, id int) ([]int, error) {
|
||||
return qb.tagsRepository().getIDs(ctx, id)
|
||||
return performerRepository.tags.getIDs(ctx, id)
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) GetImage(ctx context.Context, performerID int) ([]byte, error) {
|
||||
@@ -1218,16 +781,6 @@ func (qb *PerformerStore) destroyImage(ctx context.Context, performerID int) err
|
||||
return qb.blobJoinQueryBuilder.DestroyImage(ctx, performerID, performerImageBlobColumn)
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) stashIDRepository() *stashIDRepository {
|
||||
return &stashIDRepository{
|
||||
repository{
|
||||
tx: qb.tx,
|
||||
tableName: "performer_stash_ids",
|
||||
idColumn: performerIDColumn,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *PerformerStore) GetAliases(ctx context.Context, performerID int) ([]string, error) {
|
||||
return performersAliasesTableMgr.get(ctx, performerID)
|
||||
}
|
||||
|
||||
516
pkg/sqlite/performer_filter.go
Normal file
516
pkg/sqlite/performer_filter.go
Normal file
@@ -0,0 +1,516 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type performerFilterHandler struct {
|
||||
performerFilter *models.PerformerFilterType
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) validate() error {
|
||||
filter := qb.performerFilter
|
||||
if filter == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := validateFilterCombination(filter.OperatorFilter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if subFilter := filter.SubFilter(); subFilter != nil {
|
||||
sqb := &performerFilterHandler{performerFilter: subFilter}
|
||||
if err := sqb.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// if legacy height filter used, ensure only supported modifiers are used
|
||||
if filter.Height != nil {
|
||||
// treat as an int filter
|
||||
intCrit := &models.IntCriterionInput{
|
||||
Modifier: filter.Height.Modifier,
|
||||
}
|
||||
if !intCrit.ValidModifier() {
|
||||
return fmt.Errorf("invalid height modifier: %s", filter.Height.Modifier)
|
||||
}
|
||||
|
||||
// ensure value is a valid number
|
||||
if _, err := strconv.Atoi(filter.Height.Value); err != nil {
|
||||
return fmt.Errorf("invalid height value: %s", filter.Height.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) handle(ctx context.Context, f *filterBuilder) {
|
||||
filter := qb.performerFilter
|
||||
if filter == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := qb.validate(); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
sf := filter.SubFilter()
|
||||
if sf != nil {
|
||||
sub := &performerFilterHandler{sf}
|
||||
handleSubFilter(ctx, sub, f, filter.OperatorFilter)
|
||||
}
|
||||
|
||||
f.handleCriterion(ctx, qb.criterionHandler())
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) criterionHandler() criterionHandler {
|
||||
filter := qb.performerFilter
|
||||
const tableName = performerTable
|
||||
heightCmCrit := filter.HeightCm
|
||||
|
||||
return compoundHandler{
|
||||
stringCriterionHandler(filter.Name, tableName+".name"),
|
||||
stringCriterionHandler(filter.Disambiguation, tableName+".disambiguation"),
|
||||
stringCriterionHandler(filter.Details, tableName+".details"),
|
||||
|
||||
boolCriterionHandler(filter.FilterFavorites, tableName+".favorite", nil),
|
||||
boolCriterionHandler(filter.IgnoreAutoTag, tableName+".ignore_auto_tag", nil),
|
||||
|
||||
yearFilterCriterionHandler(filter.BirthYear, tableName+".birthdate"),
|
||||
yearFilterCriterionHandler(filter.DeathYear, tableName+".death_date"),
|
||||
|
||||
qb.performerAgeFilterCriterionHandler(filter.Age),
|
||||
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if gender := filter.Gender; gender != nil {
|
||||
genderCopy := *gender
|
||||
if genderCopy.Value.IsValid() && len(genderCopy.ValueList) == 0 {
|
||||
genderCopy.ValueList = []models.GenderEnum{genderCopy.Value}
|
||||
}
|
||||
|
||||
v := utils.StringerSliceToStringSlice(genderCopy.ValueList)
|
||||
enumCriterionHandler(genderCopy.Modifier, v, tableName+".gender")(ctx, f)
|
||||
}
|
||||
}),
|
||||
|
||||
qb.performerIsMissingCriterionHandler(filter.IsMissing),
|
||||
stringCriterionHandler(filter.Ethnicity, tableName+".ethnicity"),
|
||||
stringCriterionHandler(filter.Country, tableName+".country"),
|
||||
stringCriterionHandler(filter.EyeColor, tableName+".eye_color"),
|
||||
|
||||
// special handler for legacy height filter
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if heightCmCrit == nil && filter.Height != nil {
|
||||
heightCm, _ := strconv.Atoi(filter.Height.Value) // already validated
|
||||
heightCmCrit = &models.IntCriterionInput{
|
||||
Value: heightCm,
|
||||
Modifier: filter.Height.Modifier,
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
intCriterionHandler(heightCmCrit, tableName+".height", nil),
|
||||
|
||||
stringCriterionHandler(filter.Measurements, tableName+".measurements"),
|
||||
stringCriterionHandler(filter.FakeTits, tableName+".fake_tits"),
|
||||
floatCriterionHandler(filter.PenisLength, tableName+".penis_length", nil),
|
||||
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if circumcised := filter.Circumcised; circumcised != nil {
|
||||
v := utils.StringerSliceToStringSlice(circumcised.Value)
|
||||
enumCriterionHandler(circumcised.Modifier, v, tableName+".circumcised")(ctx, f)
|
||||
}
|
||||
}),
|
||||
|
||||
stringCriterionHandler(filter.CareerLength, tableName+".career_length"),
|
||||
stringCriterionHandler(filter.Tattoos, tableName+".tattoos"),
|
||||
stringCriterionHandler(filter.Piercings, tableName+".piercings"),
|
||||
intCriterionHandler(filter.Rating100, tableName+".rating", nil),
|
||||
stringCriterionHandler(filter.HairColor, tableName+".hair_color"),
|
||||
stringCriterionHandler(filter.URL, tableName+".url"),
|
||||
intCriterionHandler(filter.Weight, tableName+".weight", nil),
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if filter.StashID != nil {
|
||||
performerRepository.stashIDs.join(f, "performer_stash_ids", "performers.id")
|
||||
stringCriterionHandler(filter.StashID, "performer_stash_ids.stash_id")(ctx, f)
|
||||
}
|
||||
}),
|
||||
&stashIDCriterionHandler{
|
||||
c: filter.StashIDEndpoint,
|
||||
stashIDRepository: &performerRepository.stashIDs,
|
||||
stashIDTableAs: "performer_stash_ids",
|
||||
parentIDCol: "performers.id",
|
||||
},
|
||||
|
||||
qb.aliasCriterionHandler(filter.Aliases),
|
||||
|
||||
qb.tagsCriterionHandler(filter.Tags),
|
||||
|
||||
qb.studiosCriterionHandler(filter.Studios),
|
||||
|
||||
qb.appearsWithCriterionHandler(filter.Performers),
|
||||
|
||||
qb.tagCountCriterionHandler(filter.TagCount),
|
||||
qb.sceneCountCriterionHandler(filter.SceneCount),
|
||||
qb.imageCountCriterionHandler(filter.ImageCount),
|
||||
qb.galleryCountCriterionHandler(filter.GalleryCount),
|
||||
qb.playCounterCriterionHandler(filter.PlayCount),
|
||||
qb.oCounterCriterionHandler(filter.OCounter),
|
||||
&dateCriterionHandler{filter.Birthdate, tableName + ".birthdate", nil},
|
||||
&dateCriterionHandler{filter.DeathDate, tableName + ".death_date", nil},
|
||||
×tampCriterionHandler{filter.CreatedAt, tableName + ".created_at", nil},
|
||||
×tampCriterionHandler{filter.UpdatedAt, tableName + ".updated_at", nil},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "performers_scenes.scene_id",
|
||||
relatedRepo: sceneRepository.repository,
|
||||
relatedHandler: &sceneFilterHandler{filter.ScenesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
performerRepository.scenes.innerJoin(f, "", "performers.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "performers_images.image_id",
|
||||
relatedRepo: imageRepository.repository,
|
||||
relatedHandler: &imageFilterHandler{filter.ImagesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
performerRepository.images.innerJoin(f, "", "performers.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "performers_galleries.gallery_id",
|
||||
relatedRepo: galleryRepository.repository,
|
||||
relatedHandler: &galleryFilterHandler{filter.GalleriesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
performerRepository.galleries.innerJoin(f, "", "performers.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "performer_tag.tag_id",
|
||||
relatedRepo: tagRepository.repository,
|
||||
relatedHandler: &tagFilterHandler{filter.TagsFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
performerRepository.tags.innerJoin(f, "performer_tag", "performers.id")
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - we need to provide a whitelist of possible values
|
||||
func (qb *performerFilterHandler) performerIsMissingCriterionHandler(isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "scenes": // Deprecated: use `scene_count == 0` filter instead
|
||||
f.addLeftJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id")
|
||||
f.addWhere("scenes_join.scene_id IS NULL")
|
||||
case "image":
|
||||
f.addWhere("performers.image_blob IS NULL")
|
||||
case "stash_id":
|
||||
performersStashIDsTableMgr.join(f, "performer_stash_ids", "performers.id")
|
||||
f.addWhere("performer_stash_ids.performer_id IS NULL")
|
||||
case "aliases":
|
||||
performersAliasesTableMgr.join(f, "", "performers.id")
|
||||
f.addWhere("performer_aliases.alias IS NULL")
|
||||
default:
|
||||
f.addWhere("(performers." + *isMissing + " IS NULL OR TRIM(performers." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) performerAgeFilterCriterionHandler(age *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if age != nil && age.Modifier.IsValid() {
|
||||
clause, args := getIntCriterionWhereClause(
|
||||
"cast(IFNULL(strftime('%Y.%m%d', performers.death_date), strftime('%Y.%m%d', 'now')) - strftime('%Y.%m%d', performers.birthdate) as int)",
|
||||
*age,
|
||||
)
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) aliasCriterionHandler(alias *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: performersAliasesTable,
|
||||
stringColumn: performerAliasColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
performersAliasesTableMgr.join(f, "", "performers.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(alias)
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) tagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedHierarchicalMultiCriterionHandlerBuilder{
|
||||
primaryTable: performerTable,
|
||||
foreignTable: tagTable,
|
||||
foreignFK: "tag_id",
|
||||
|
||||
relationsTable: "tags_relations",
|
||||
joinAs: "performer_tag",
|
||||
joinTable: performersTagsTable,
|
||||
primaryFK: performerIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(tags)
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) tagCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: performerTable,
|
||||
joinTable: performersTagsTable,
|
||||
primaryFK: performerIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(count)
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) sceneCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: performerTable,
|
||||
joinTable: performersScenesTable,
|
||||
primaryFK: performerIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(count)
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) imageCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: performerTable,
|
||||
joinTable: performersImagesTable,
|
||||
primaryFK: performerIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(count)
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) galleryCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: performerTable,
|
||||
joinTable: performersGalleriesTable,
|
||||
primaryFK: performerIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(count)
|
||||
}
|
||||
|
||||
// used for sorting and filtering on performer o-count
|
||||
var selectPerformerOCountSQL = utils.StrFormat(
|
||||
"SELECT SUM(o_counter) "+
|
||||
"FROM ("+
|
||||
"SELECT SUM(o_counter) as o_counter from {performers_images} s "+
|
||||
"LEFT JOIN {images} ON {images}.id = s.{images_id} "+
|
||||
"WHERE s.{performer_id} = {performers}.id "+
|
||||
"UNION ALL "+
|
||||
"SELECT COUNT({scenes_o_dates}.{o_date}) as o_counter from {performers_scenes} s "+
|
||||
"LEFT JOIN {scenes} ON {scenes}.id = s.{scene_id} "+
|
||||
"LEFT JOIN {scenes_o_dates} ON {scenes_o_dates}.{scene_id} = {scenes}.id "+
|
||||
"WHERE s.{performer_id} = {performers}.id "+
|
||||
")",
|
||||
map[string]interface{}{
|
||||
"performers_images": performersImagesTable,
|
||||
"images": imageTable,
|
||||
"performer_id": performerIDColumn,
|
||||
"images_id": imageIDColumn,
|
||||
"performers": performerTable,
|
||||
"performers_scenes": performersScenesTable,
|
||||
"scenes": sceneTable,
|
||||
"scene_id": sceneIDColumn,
|
||||
"scenes_o_dates": scenesODatesTable,
|
||||
"o_date": sceneODateColumn,
|
||||
},
|
||||
)
|
||||
|
||||
// used for sorting and filtering play count on performer view count
|
||||
var selectPerformerPlayCountSQL = utils.StrFormat(
|
||||
"SELECT COUNT(DISTINCT {view_date}) FROM ("+
|
||||
"SELECT {view_date} FROM {performers_scenes} s "+
|
||||
"LEFT JOIN {scenes} ON {scenes}.id = s.{scene_id} "+
|
||||
"LEFT JOIN {scenes_view_dates} ON {scenes_view_dates}.{scene_id} = {scenes}.id "+
|
||||
"WHERE s.{performer_id} = {performers}.id"+
|
||||
")",
|
||||
map[string]interface{}{
|
||||
"performer_id": performerIDColumn,
|
||||
"performers": performerTable,
|
||||
"performers_scenes": performersScenesTable,
|
||||
"scenes": sceneTable,
|
||||
"scene_id": sceneIDColumn,
|
||||
"scenes_view_dates": scenesViewDatesTable,
|
||||
"view_date": sceneViewDateColumn,
|
||||
},
|
||||
)
|
||||
|
||||
func (qb *performerFilterHandler) oCounterCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if count == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lhs := "(" + selectPerformerOCountSQL + ")"
|
||||
clause, args := getIntCriterionWhereClause(lhs, *count)
|
||||
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) playCounterCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if count == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lhs := "(" + selectPerformerPlayCountSQL + ")"
|
||||
clause, args := getIntCriterionWhereClause(lhs, *count)
|
||||
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) studiosCriterionHandler(studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if studios != nil {
|
||||
formatMaps := []utils.StrFormatMap{
|
||||
{
|
||||
"primaryTable": sceneTable,
|
||||
"joinTable": performersScenesTable,
|
||||
"primaryFK": sceneIDColumn,
|
||||
},
|
||||
{
|
||||
"primaryTable": imageTable,
|
||||
"joinTable": performersImagesTable,
|
||||
"primaryFK": imageIDColumn,
|
||||
},
|
||||
{
|
||||
"primaryTable": galleryTable,
|
||||
"joinTable": performersGalleriesTable,
|
||||
"primaryFK": galleryIDColumn,
|
||||
},
|
||||
}
|
||||
|
||||
if studios.Modifier == models.CriterionModifierIsNull || studios.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if studios.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
var conditions []string
|
||||
for _, c := range formatMaps {
|
||||
f.addLeftJoin(c["joinTable"].(string), "", fmt.Sprintf("%s.performer_id = performers.id", c["joinTable"]))
|
||||
f.addLeftJoin(c["primaryTable"].(string), "", fmt.Sprintf("%s.%s = %s.id", c["joinTable"], c["primaryFK"], c["primaryTable"]))
|
||||
|
||||
conditions = append(conditions, fmt.Sprintf("%s.studio_id IS NULL", c["primaryTable"]))
|
||||
}
|
||||
|
||||
f.addWhere(fmt.Sprintf("%s (%s)", notClause, strings.Join(conditions, " AND ")))
|
||||
return
|
||||
}
|
||||
|
||||
if len(studios.Value) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var clauseCondition string
|
||||
|
||||
switch studios.Modifier {
|
||||
case models.CriterionModifierIncludes:
|
||||
// return performers who appear in scenes/images/galleries with any of the given studios
|
||||
clauseCondition = "NOT"
|
||||
case models.CriterionModifierExcludes:
|
||||
// exclude performers who appear in scenes/images/galleries with any of the given studios
|
||||
clauseCondition = ""
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
const derivedPerformerStudioTable = "performer_studio"
|
||||
valuesClause, err := getHierarchicalValues(ctx, studios.Value, studioTable, "", "parent_id", "child_id", studios.Depth)
|
||||
if err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
f.addWith("studio(root_id, item_id) AS (" + valuesClause + ")")
|
||||
|
||||
templStr := `SELECT performer_id FROM {primaryTable}
|
||||
INNER JOIN {joinTable} ON {primaryTable}.id = {joinTable}.{primaryFK}
|
||||
INNER JOIN studio ON {primaryTable}.studio_id = studio.item_id`
|
||||
|
||||
var unions []string
|
||||
for _, c := range formatMaps {
|
||||
unions = append(unions, utils.StrFormat(templStr, c))
|
||||
}
|
||||
|
||||
f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerStudioTable, strings.Join(unions, " UNION ")))
|
||||
|
||||
f.addLeftJoin(derivedPerformerStudioTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerStudioTable))
|
||||
f.addWhere(fmt.Sprintf("%s.performer_id IS %s NULL", derivedPerformerStudioTable, clauseCondition))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *performerFilterHandler) appearsWithCriterionHandler(performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performers != nil {
|
||||
formatMaps := []utils.StrFormatMap{
|
||||
{
|
||||
"primaryTable": performersScenesTable,
|
||||
"joinTable": performersScenesTable,
|
||||
"primaryFK": sceneIDColumn,
|
||||
},
|
||||
{
|
||||
"primaryTable": performersImagesTable,
|
||||
"joinTable": performersImagesTable,
|
||||
"primaryFK": imageIDColumn,
|
||||
},
|
||||
{
|
||||
"primaryTable": performersGalleriesTable,
|
||||
"joinTable": performersGalleriesTable,
|
||||
"primaryFK": galleryIDColumn,
|
||||
},
|
||||
}
|
||||
|
||||
if len(performers.Value) == '0' {
|
||||
return
|
||||
}
|
||||
|
||||
const derivedPerformerPerformersTable = "performer_performers"
|
||||
|
||||
valuesClause := strings.Join(performers.Value, "),(")
|
||||
|
||||
f.addWith("performer(id) AS (VALUES(" + valuesClause + "))")
|
||||
|
||||
templStr := `SELECT {primaryTable}2.performer_id FROM {primaryTable}
|
||||
INNER JOIN {primaryTable} AS {primaryTable}2 ON {primaryTable}.{primaryFK} = {primaryTable}2.{primaryFK}
|
||||
INNER JOIN performer ON {primaryTable}.performer_id = performer.id
|
||||
WHERE {primaryTable}2.performer_id != performer.id`
|
||||
|
||||
if performers.Modifier == models.CriterionModifierIncludesAll && len(performers.Value) > 1 {
|
||||
templStr += `
|
||||
GROUP BY {primaryTable}2.performer_id
|
||||
HAVING(count(distinct {primaryTable}.performer_id) IS ` + strconv.Itoa(len(performers.Value)) + `)`
|
||||
}
|
||||
|
||||
var unions []string
|
||||
for _, c := range formatMaps {
|
||||
unions = append(unions, utils.StrFormat(templStr, c))
|
||||
}
|
||||
|
||||
f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerPerformersTable, strings.Join(unions, " UNION ")))
|
||||
|
||||
f.addInnerJoin(derivedPerformerPerformersTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerPerformersTable))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -731,10 +731,12 @@ func TestPerformerQueryEthnicityOr(t *testing.T) {
|
||||
Value: performer1Eth,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
Or: &models.PerformerFilterType{
|
||||
Ethnicity: &models.StringCriterionInput{
|
||||
Value: performer2Eth,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
OperatorFilter: models.OperatorFilter[models.PerformerFilterType]{
|
||||
Or: &models.PerformerFilterType{
|
||||
Ethnicity: &models.StringCriterionInput{
|
||||
Value: performer2Eth,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -760,10 +762,12 @@ func TestPerformerQueryEthnicityAndRating(t *testing.T) {
|
||||
Value: performerEth,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
And: &models.PerformerFilterType{
|
||||
Rating100: &models.IntCriterionInput{
|
||||
Value: performerRating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
OperatorFilter: models.OperatorFilter[models.PerformerFilterType]{
|
||||
And: &models.PerformerFilterType{
|
||||
Rating100: &models.IntCriterionInput{
|
||||
Value: performerRating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -801,8 +805,10 @@ func TestPerformerQueryEthnicityNotRating(t *testing.T) {
|
||||
|
||||
performerFilter := models.PerformerFilterType{
|
||||
Ethnicity: ðCriterion,
|
||||
Not: &models.PerformerFilterType{
|
||||
Rating100: &ratingCriterion,
|
||||
OperatorFilter: models.OperatorFilter[models.PerformerFilterType]{
|
||||
Not: &models.PerformerFilterType{
|
||||
Rating100: &ratingCriterion,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -838,24 +844,30 @@ func TestPerformerIllegalQuery(t *testing.T) {
|
||||
// And and Or in the same filter
|
||||
"AndOr",
|
||||
models.PerformerFilterType{
|
||||
And: &subFilter,
|
||||
Or: &subFilter,
|
||||
OperatorFilter: models.OperatorFilter[models.PerformerFilterType]{
|
||||
And: &subFilter,
|
||||
Or: &subFilter,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// And and Not in the same filter
|
||||
"AndNot",
|
||||
models.PerformerFilterType{
|
||||
And: &subFilter,
|
||||
Not: &subFilter,
|
||||
OperatorFilter: models.OperatorFilter[models.PerformerFilterType]{
|
||||
And: &subFilter,
|
||||
Not: &subFilter,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Or and Not in the same filter
|
||||
"OrNot",
|
||||
models.PerformerFilterType{
|
||||
Or: &subFilter,
|
||||
Not: &subFilter,
|
||||
OperatorFilter: models.OperatorFilter[models.PerformerFilterType]{
|
||||
Or: &subFilter,
|
||||
Not: &subFilter,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -20,7 +20,6 @@ type objectList interface {
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
tx dbWrapper
|
||||
tableName string
|
||||
idColumn string
|
||||
}
|
||||
@@ -48,7 +47,7 @@ func (r *repository) destroyExisting(ctx context.Context, ids []int) error {
|
||||
func (r *repository) destroy(ctx context.Context, ids []int) error {
|
||||
for _, id := range ids {
|
||||
stmt := fmt.Sprintf("DELETE FROM %s WHERE %s = ?", r.tableName, r.idColumn)
|
||||
if _, err := r.tx.Exec(ctx, stmt, id); err != nil {
|
||||
if _, err := dbWrapper.Exec(ctx, stmt, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -78,7 +77,7 @@ func (r *repository) runCountQuery(ctx context.Context, query string, args []int
|
||||
}{0}
|
||||
|
||||
// Perform query and fetch result
|
||||
if err := r.tx.Get(ctx, &result, query, args...); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
if err := dbWrapper.Get(ctx, &result, query, args...); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -90,7 +89,7 @@ func (r *repository) runIdsQuery(ctx context.Context, query string, args []inter
|
||||
Int int `db:"id"`
|
||||
}
|
||||
|
||||
if err := r.tx.Select(ctx, &result, query, args...); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
if err := dbWrapper.Select(ctx, &result, query, args...); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return []int{}, fmt.Errorf("running query: %s [%v]: %w", query, args, err)
|
||||
}
|
||||
|
||||
@@ -102,7 +101,7 @@ func (r *repository) runIdsQuery(ctx context.Context, query string, args []inter
|
||||
}
|
||||
|
||||
func (r *repository) queryFunc(ctx context.Context, query string, args []interface{}, single bool, f func(rows *sqlx.Rows) error) error {
|
||||
rows, err := r.tx.Queryx(ctx, query, args...)
|
||||
rows, err := dbWrapper.Queryx(ctx, query, args...)
|
||||
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
@@ -150,7 +149,7 @@ func (r *repository) queryStruct(ctx context.Context, query string, args []inter
|
||||
}
|
||||
|
||||
func (r *repository) querySimple(ctx context.Context, query string, args []interface{}, out interface{}) error {
|
||||
rows, err := r.tx.Queryx(ctx, query, args...)
|
||||
rows, err := dbWrapper.Queryx(ctx, query, args...)
|
||||
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
@@ -230,7 +229,6 @@ func (r *repository) join(j joiner, as string, parentIDCol string) {
|
||||
j.addLeftJoin(r.tableName, as, fmt.Sprintf("%s.%s = %s", t, r.idColumn, parentIDCol))
|
||||
}
|
||||
|
||||
//nolint:golint,unused
|
||||
func (r *repository) innerJoin(j joiner, as string, parentIDCol string) {
|
||||
t := r.tableName
|
||||
if as != "" {
|
||||
@@ -269,7 +267,7 @@ func (r *joinRepository) getIDs(ctx context.Context, id int) ([]int, error) {
|
||||
}
|
||||
|
||||
func (r *joinRepository) insert(ctx context.Context, id int, foreignIDs ...int) error {
|
||||
stmt, err := r.tx.Prepare(ctx, fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?)", r.tableName, r.idColumn, r.fkColumn))
|
||||
stmt, err := dbWrapper.Prepare(ctx, fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?)", r.tableName, r.idColumn, r.fkColumn))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -277,7 +275,7 @@ func (r *joinRepository) insert(ctx context.Context, id int, foreignIDs ...int)
|
||||
defer stmt.Close()
|
||||
|
||||
for _, fk := range foreignIDs {
|
||||
if _, err := r.tx.ExecStmt(ctx, stmt, id, fk); err != nil {
|
||||
if _, err := dbWrapper.ExecStmt(ctx, stmt, id, fk); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -286,7 +284,7 @@ func (r *joinRepository) insert(ctx context.Context, id int, foreignIDs ...int)
|
||||
|
||||
// insertOrIgnore inserts a join into the table, silently failing in the event that a conflict occurs (ie when the join already exists)
|
||||
func (r *joinRepository) insertOrIgnore(ctx context.Context, id int, foreignIDs ...int) error {
|
||||
stmt, err := r.tx.Prepare(ctx, fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?) ON CONFLICT (%[2]s, %s) DO NOTHING", r.tableName, r.idColumn, r.fkColumn))
|
||||
stmt, err := dbWrapper.Prepare(ctx, fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?) ON CONFLICT (%[2]s, %s) DO NOTHING", r.tableName, r.idColumn, r.fkColumn))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -294,7 +292,7 @@ func (r *joinRepository) insertOrIgnore(ctx context.Context, id int, foreignIDs
|
||||
defer stmt.Close()
|
||||
|
||||
for _, fk := range foreignIDs {
|
||||
if _, err := r.tx.ExecStmt(ctx, stmt, id, fk); err != nil {
|
||||
if _, err := dbWrapper.ExecStmt(ctx, stmt, id, fk); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -310,7 +308,7 @@ func (r *joinRepository) destroyJoins(ctx context.Context, id int, foreignIDs ..
|
||||
args[i+1] = v
|
||||
}
|
||||
|
||||
if _, err := r.tx.Exec(ctx, stmt, args...); err != nil {
|
||||
if _, err := dbWrapper.Exec(ctx, stmt, args...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -360,7 +358,7 @@ func (r *captionRepository) get(ctx context.Context, id models.FileID) ([]*model
|
||||
|
||||
func (r *captionRepository) insert(ctx context.Context, id models.FileID, caption *models.VideoCaption) (sql.Result, error) {
|
||||
stmt := fmt.Sprintf("INSERT INTO %s (%s, %s, %s, %s) VALUES (?, ?, ?, ?)", r.tableName, r.idColumn, captionCodeColumn, captionFilenameColumn, captionTypeColumn)
|
||||
return r.tx.Exec(ctx, stmt, id, caption.LanguageCode, caption.Filename, caption.CaptionType)
|
||||
return dbWrapper.Exec(ctx, stmt, id, caption.LanguageCode, caption.Filename, caption.CaptionType)
|
||||
}
|
||||
|
||||
func (r *captionRepository) replace(ctx context.Context, id models.FileID, captions []*models.VideoCaption) error {
|
||||
@@ -399,7 +397,7 @@ func (r *stringRepository) get(ctx context.Context, id int) ([]string, error) {
|
||||
|
||||
func (r *stringRepository) insert(ctx context.Context, id int, s string) (sql.Result, error) {
|
||||
stmt := fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?)", r.tableName, r.idColumn, r.stringColumn)
|
||||
return r.tx.Exec(ctx, stmt, id, s)
|
||||
return dbWrapper.Exec(ctx, stmt, id, s)
|
||||
}
|
||||
|
||||
func (r *stringRepository) replace(ctx context.Context, id int, newStrings []string) error {
|
||||
|
||||
@@ -168,23 +168,78 @@ func (r *sceneRowRecord) fromPartial(o models.ScenePartial) {
|
||||
r.setFloat64("play_duration", o.PlayDuration)
|
||||
}
|
||||
|
||||
type SceneStore struct {
|
||||
type sceneRepositoryType struct {
|
||||
repository
|
||||
galleries joinRepository
|
||||
tags joinRepository
|
||||
performers joinRepository
|
||||
movies repository
|
||||
|
||||
files filesRepository
|
||||
|
||||
stashIDs stashIDRepository
|
||||
}
|
||||
|
||||
var (
|
||||
sceneRepository = sceneRepositoryType{
|
||||
repository: repository{
|
||||
tableName: sceneTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
galleries: joinRepository{
|
||||
repository: repository{
|
||||
tableName: scenesGalleriesTable,
|
||||
idColumn: sceneIDColumn,
|
||||
},
|
||||
fkColumn: galleryIDColumn,
|
||||
},
|
||||
tags: joinRepository{
|
||||
repository: repository{
|
||||
tableName: scenesTagsTable,
|
||||
idColumn: sceneIDColumn,
|
||||
},
|
||||
fkColumn: tagIDColumn,
|
||||
foreignTable: tagTable,
|
||||
orderBy: "tags.name ASC",
|
||||
},
|
||||
performers: joinRepository{
|
||||
repository: repository{
|
||||
tableName: performersScenesTable,
|
||||
idColumn: sceneIDColumn,
|
||||
},
|
||||
fkColumn: performerIDColumn,
|
||||
},
|
||||
movies: repository{
|
||||
tableName: moviesScenesTable,
|
||||
idColumn: sceneIDColumn,
|
||||
},
|
||||
files: filesRepository{
|
||||
repository: repository{
|
||||
tableName: scenesFilesTable,
|
||||
idColumn: sceneIDColumn,
|
||||
},
|
||||
},
|
||||
stashIDs: stashIDRepository{
|
||||
repository{
|
||||
tableName: "scene_stash_ids",
|
||||
idColumn: sceneIDColumn,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type SceneStore struct {
|
||||
blobJoinQueryBuilder
|
||||
|
||||
tableMgr *table
|
||||
oDateManager
|
||||
viewDateManager
|
||||
|
||||
fileStore *FileStore
|
||||
repo *storeRepository
|
||||
}
|
||||
|
||||
func NewSceneStore(fileStore *FileStore, blobStore *BlobStore) *SceneStore {
|
||||
func NewSceneStore(r *storeRepository, blobStore *BlobStore) *SceneStore {
|
||||
return &SceneStore{
|
||||
repository: repository{
|
||||
tableName: sceneTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
blobJoinQueryBuilder: blobJoinQueryBuilder{
|
||||
blobStore: blobStore,
|
||||
joinTable: sceneTable,
|
||||
@@ -193,7 +248,7 @@ func NewSceneStore(fileStore *FileStore, blobStore *BlobStore) *SceneStore {
|
||||
tableMgr: sceneTableMgr,
|
||||
viewDateManager: viewDateManager{scenesViewTableMgr},
|
||||
oDateManager: oDateManager{scenesOTableMgr},
|
||||
fileStore: fileStore,
|
||||
repo: r,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,13 +586,13 @@ func (qb *SceneStore) getMany(ctx context.Context, q *goqu.SelectDataset) ([]*mo
|
||||
}
|
||||
|
||||
func (qb *SceneStore) GetFiles(ctx context.Context, id int) ([]*models.VideoFile, error) {
|
||||
fileIDs, err := qb.filesRepository().get(ctx, id)
|
||||
fileIDs, err := sceneRepository.files.get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// use fileStore to load files
|
||||
files, err := qb.fileStore.Find(ctx, fileIDs...)
|
||||
files, err := qb.repo.File.Find(ctx, fileIDs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -556,7 +611,7 @@ func (qb *SceneStore) GetFiles(ctx context.Context, id int) ([]*models.VideoFile
|
||||
|
||||
func (qb *SceneStore) GetManyFileIDs(ctx context.Context, ids []int) ([][]models.FileID, error) {
|
||||
const primaryOnly = false
|
||||
return qb.filesRepository().getMany(ctx, ids, primaryOnly)
|
||||
return sceneRepository.files.getMany(ctx, ids, primaryOnly)
|
||||
}
|
||||
|
||||
func (qb *SceneStore) FindByFileID(ctx context.Context, fileID models.FileID) ([]*models.Scene, error) {
|
||||
@@ -864,176 +919,6 @@ func (qb *SceneStore) All(ctx context.Context) ([]*models.Scene, error) {
|
||||
))
|
||||
}
|
||||
|
||||
func illegalFilterCombination(type1, type2 string) error {
|
||||
return fmt.Errorf("cannot have %s and %s in the same filter", type1, type2)
|
||||
}
|
||||
|
||||
func (qb *SceneStore) validateFilter(sceneFilter *models.SceneFilterType) error {
|
||||
const and = "AND"
|
||||
const or = "OR"
|
||||
const not = "NOT"
|
||||
|
||||
if sceneFilter.And != nil {
|
||||
if sceneFilter.Or != nil {
|
||||
return illegalFilterCombination(and, or)
|
||||
}
|
||||
if sceneFilter.Not != nil {
|
||||
return illegalFilterCombination(and, not)
|
||||
}
|
||||
|
||||
return qb.validateFilter(sceneFilter.And)
|
||||
}
|
||||
|
||||
if sceneFilter.Or != nil {
|
||||
if sceneFilter.Not != nil {
|
||||
return illegalFilterCombination(or, not)
|
||||
}
|
||||
|
||||
return qb.validateFilter(sceneFilter.Or)
|
||||
}
|
||||
|
||||
if sceneFilter.Not != nil {
|
||||
return qb.validateFilter(sceneFilter.Not)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneFilterType) *filterBuilder {
|
||||
query := &filterBuilder{}
|
||||
|
||||
if sceneFilter.And != nil {
|
||||
query.and(qb.makeFilter(ctx, sceneFilter.And))
|
||||
}
|
||||
if sceneFilter.Or != nil {
|
||||
query.or(qb.makeFilter(ctx, sceneFilter.Or))
|
||||
}
|
||||
if sceneFilter.Not != nil {
|
||||
query.not(qb.makeFilter(ctx, sceneFilter.Not))
|
||||
}
|
||||
|
||||
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.ID, "scenes.id", nil))
|
||||
query.handleCriterion(ctx, pathCriterionHandler(sceneFilter.Path, "folders.path", "files.basename", qb.addFoldersTable))
|
||||
query.handleCriterion(ctx, sceneFileCountCriterionHandler(qb, sceneFilter.FileCount))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(sceneFilter.Title, "scenes.title"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(sceneFilter.Code, "scenes.code"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(sceneFilter.Details, "scenes.details"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(sceneFilter.Director, "scenes.director"))
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if sceneFilter.Oshash != nil {
|
||||
qb.addSceneFilesTable(f)
|
||||
f.addLeftJoin(fingerprintTable, "fingerprints_oshash", "scenes_files.file_id = fingerprints_oshash.file_id AND fingerprints_oshash.type = 'oshash'")
|
||||
}
|
||||
|
||||
stringCriterionHandler(sceneFilter.Oshash, "fingerprints_oshash.fingerprint")(ctx, f)
|
||||
}))
|
||||
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if sceneFilter.Checksum != nil {
|
||||
qb.addSceneFilesTable(f)
|
||||
f.addLeftJoin(fingerprintTable, "fingerprints_md5", "scenes_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
|
||||
}
|
||||
|
||||
stringCriterionHandler(sceneFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
|
||||
}))
|
||||
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if sceneFilter.Phash != nil {
|
||||
// backwards compatibility
|
||||
scenePhashDistanceCriterionHandler(qb, &models.PhashDistanceCriterionInput{
|
||||
Value: sceneFilter.Phash.Value,
|
||||
Modifier: sceneFilter.Phash.Modifier,
|
||||
})(ctx, f)
|
||||
}
|
||||
}))
|
||||
|
||||
query.handleCriterion(ctx, scenePhashDistanceCriterionHandler(qb, sceneFilter.PhashDistance))
|
||||
|
||||
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.Rating100, "scenes.rating", nil))
|
||||
query.handleCriterion(ctx, sceneOCountCriterionHandler(sceneFilter.OCounter))
|
||||
query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Organized, "scenes.organized", nil))
|
||||
|
||||
query.handleCriterion(ctx, floatIntCriterionHandler(sceneFilter.Duration, "video_files.duration", qb.addVideoFilesTable))
|
||||
query.handleCriterion(ctx, resolutionCriterionHandler(sceneFilter.Resolution, "video_files.height", "video_files.width", qb.addVideoFilesTable))
|
||||
query.handleCriterion(ctx, orientationCriterionHandler(sceneFilter.Orientation, "video_files.height", "video_files.width", qb.addVideoFilesTable))
|
||||
query.handleCriterion(ctx, floatIntCriterionHandler(sceneFilter.Framerate, "ROUND(video_files.frame_rate)", qb.addVideoFilesTable))
|
||||
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.Bitrate, "video_files.bit_rate", qb.addVideoFilesTable))
|
||||
query.handleCriterion(ctx, codecCriterionHandler(sceneFilter.VideoCodec, "video_files.video_codec", qb.addVideoFilesTable))
|
||||
query.handleCriterion(ctx, codecCriterionHandler(sceneFilter.AudioCodec, "video_files.audio_codec", qb.addVideoFilesTable))
|
||||
|
||||
query.handleCriterion(ctx, hasMarkersCriterionHandler(sceneFilter.HasMarkers))
|
||||
query.handleCriterion(ctx, sceneIsMissingCriterionHandler(qb, sceneFilter.IsMissing))
|
||||
query.handleCriterion(ctx, sceneURLsCriterionHandler(sceneFilter.URL))
|
||||
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if sceneFilter.StashID != nil {
|
||||
qb.stashIDRepository().join(f, "scene_stash_ids", "scenes.id")
|
||||
stringCriterionHandler(sceneFilter.StashID, "scene_stash_ids.stash_id")(ctx, f)
|
||||
}
|
||||
}))
|
||||
query.handleCriterion(ctx, &stashIDCriterionHandler{
|
||||
c: sceneFilter.StashIDEndpoint,
|
||||
stashIDRepository: qb.stashIDRepository(),
|
||||
stashIDTableAs: "scene_stash_ids",
|
||||
parentIDCol: "scenes.id",
|
||||
})
|
||||
|
||||
query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Interactive, "video_files.interactive", qb.addVideoFilesTable))
|
||||
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.InteractiveSpeed, "video_files.interactive_speed", qb.addVideoFilesTable))
|
||||
|
||||
query.handleCriterion(ctx, sceneCaptionCriterionHandler(qb, sceneFilter.Captions))
|
||||
|
||||
query.handleCriterion(ctx, floatIntCriterionHandler(sceneFilter.ResumeTime, "scenes.resume_time", nil))
|
||||
query.handleCriterion(ctx, floatIntCriterionHandler(sceneFilter.PlayDuration, "scenes.play_duration", nil))
|
||||
query.handleCriterion(ctx, scenePlayCountCriterionHandler(sceneFilter.PlayCount))
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if sceneFilter.LastPlayedAt != nil {
|
||||
f.addLeftJoin(
|
||||
fmt.Sprintf("(SELECT %s, MAX(%s) as last_played_at FROM %s GROUP BY %s)", sceneIDColumn, sceneViewDateColumn, scenesViewDatesTable, sceneIDColumn),
|
||||
"scene_last_view",
|
||||
fmt.Sprintf("scene_last_view.%s = scenes.id", sceneIDColumn),
|
||||
)
|
||||
timestampCriterionHandler(sceneFilter.LastPlayedAt, "IFNULL(last_played_at, datetime(0))")(ctx, f)
|
||||
}
|
||||
}))
|
||||
|
||||
query.handleCriterion(ctx, sceneTagsCriterionHandler(qb, sceneFilter.Tags))
|
||||
query.handleCriterion(ctx, sceneTagCountCriterionHandler(qb, sceneFilter.TagCount))
|
||||
query.handleCriterion(ctx, scenePerformersCriterionHandler(qb, sceneFilter.Performers))
|
||||
query.handleCriterion(ctx, scenePerformerCountCriterionHandler(qb, sceneFilter.PerformerCount))
|
||||
query.handleCriterion(ctx, studioCriterionHandler(sceneTable, sceneFilter.Studios))
|
||||
query.handleCriterion(ctx, sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
|
||||
query.handleCriterion(ctx, sceneGalleriesCriterionHandler(qb, sceneFilter.Galleries))
|
||||
query.handleCriterion(ctx, scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags))
|
||||
query.handleCriterion(ctx, scenePerformerFavoriteCriterionHandler(sceneFilter.PerformerFavorite))
|
||||
query.handleCriterion(ctx, scenePerformerAgeCriterionHandler(sceneFilter.PerformerAge))
|
||||
query.handleCriterion(ctx, scenePhashDuplicatedCriterionHandler(sceneFilter.Duplicated, qb.addSceneFilesTable))
|
||||
query.handleCriterion(ctx, dateCriterionHandler(sceneFilter.Date, "scenes.date"))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(sceneFilter.CreatedAt, "scenes.created_at"))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(sceneFilter.UpdatedAt, "scenes.updated_at"))
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (qb *SceneStore) addSceneFilesTable(f *filterBuilder) {
|
||||
f.addLeftJoin(scenesFilesTable, "", "scenes_files.scene_id = scenes.id")
|
||||
}
|
||||
|
||||
func (qb *SceneStore) addFilesTable(f *filterBuilder) {
|
||||
qb.addSceneFilesTable(f)
|
||||
f.addLeftJoin(fileTable, "", "scenes_files.file_id = files.id")
|
||||
}
|
||||
|
||||
func (qb *SceneStore) addFoldersTable(f *filterBuilder) {
|
||||
qb.addFilesTable(f)
|
||||
f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id")
|
||||
}
|
||||
|
||||
func (qb *SceneStore) addVideoFilesTable(f *filterBuilder) {
|
||||
qb.addSceneFilesTable(f)
|
||||
f.addLeftJoin(videoFileTable, "", "video_files.file_id = scenes_files.file_id")
|
||||
}
|
||||
|
||||
func (qb *SceneStore) makeQuery(ctx context.Context, sceneFilter *models.SceneFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
|
||||
if sceneFilter == nil {
|
||||
sceneFilter = &models.SceneFilterType{}
|
||||
@@ -1042,7 +927,7 @@ func (qb *SceneStore) makeQuery(ctx context.Context, sceneFilter *models.SceneFi
|
||||
findFilter = &models.FindFilterType{}
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
query := sceneRepository.newQuery()
|
||||
distinctIDs(&query, sceneTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
@@ -1074,10 +959,9 @@ func (qb *SceneStore) makeQuery(ctx context.Context, sceneFilter *models.SceneFi
|
||||
query.parseQueryString(searchColumns, *q)
|
||||
}
|
||||
|
||||
if err := qb.validateFilter(sceneFilter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter := qb.makeFilter(ctx, sceneFilter)
|
||||
filter := filterBuilderFromHandler(ctx, &sceneFilterHandler{
|
||||
sceneFilter: sceneFilter,
|
||||
})
|
||||
|
||||
if err := query.addFilter(filter); err != nil {
|
||||
return nil, err
|
||||
@@ -1117,7 +1001,7 @@ func (qb *SceneStore) queryGroupedFields(ctx context.Context, options models.Sce
|
||||
return models.NewSceneQueryResult(qb), nil
|
||||
}
|
||||
|
||||
aggregateQuery := qb.newQuery()
|
||||
aggregateQuery := sceneRepository.newQuery()
|
||||
|
||||
if options.Count {
|
||||
aggregateQuery.addColumn("COUNT(DISTINCT temp.id) as total")
|
||||
@@ -1161,7 +1045,7 @@ func (qb *SceneStore) queryGroupedFields(ctx context.Context, options models.Sce
|
||||
Duration null.Float
|
||||
Size null.Float
|
||||
}{}
|
||||
if err := qb.repository.queryStruct(ctx, aggregateQuery.toSQL(includeSortPagination), query.args, &out); err != nil {
|
||||
if err := sceneRepository.queryStruct(ctx, aggregateQuery.toSQL(includeSortPagination), query.args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1181,349 +1065,6 @@ func (qb *SceneStore) QueryCount(ctx context.Context, sceneFilter *models.SceneF
|
||||
return query.executeCount(ctx)
|
||||
}
|
||||
|
||||
func scenePlayCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
joinTable: scenesViewDatesTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(count)
|
||||
}
|
||||
|
||||
func sceneOCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
joinTable: scenesODatesTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(count)
|
||||
}
|
||||
|
||||
func sceneFileCountCriterionHandler(qb *SceneStore, fileCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
joinTable: scenesFilesTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(fileCount)
|
||||
}
|
||||
|
||||
func scenePhashDuplicatedCriterionHandler(duplicatedFilter *models.PHashDuplicationCriterionInput, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
// TODO: Wishlist item: Implement Distance matching
|
||||
if duplicatedFilter != nil {
|
||||
if addJoinFn != nil {
|
||||
addJoinFn(f)
|
||||
}
|
||||
|
||||
var v string
|
||||
if *duplicatedFilter.Duplicated {
|
||||
v = ">"
|
||||
} else {
|
||||
v = "="
|
||||
}
|
||||
|
||||
f.addInnerJoin("(SELECT file_id FROM files_fingerprints INNER JOIN (SELECT fingerprint FROM files_fingerprints WHERE type = 'phash' GROUP BY fingerprint HAVING COUNT (fingerprint) "+v+" 1) dupes on files_fingerprints.fingerprint = dupes.fingerprint)", "scph", "scenes_files.file_id = scph.file_id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func floatIntCriterionHandler(durationFilter *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if durationFilter != nil {
|
||||
if addJoinFn != nil {
|
||||
addJoinFn(f)
|
||||
}
|
||||
clause, args := getIntCriterionWhereClause("cast("+column+" as int)", *durationFilter)
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolutionCriterionHandler(resolution *models.ResolutionCriterionInput, heightColumn string, widthColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if resolution != nil && resolution.Value.IsValid() {
|
||||
if addJoinFn != nil {
|
||||
addJoinFn(f)
|
||||
}
|
||||
|
||||
min := resolution.Value.GetMinResolution()
|
||||
max := resolution.Value.GetMaxResolution()
|
||||
|
||||
widthHeight := fmt.Sprintf("MIN(%s, %s)", widthColumn, heightColumn)
|
||||
|
||||
switch resolution.Modifier {
|
||||
case models.CriterionModifierEquals:
|
||||
f.addWhere(fmt.Sprintf("%s BETWEEN %d AND %d", widthHeight, min, max))
|
||||
case models.CriterionModifierNotEquals:
|
||||
f.addWhere(fmt.Sprintf("%s NOT BETWEEN %d AND %d", widthHeight, min, max))
|
||||
case models.CriterionModifierLessThan:
|
||||
f.addWhere(fmt.Sprintf("%s < %d", widthHeight, min))
|
||||
case models.CriterionModifierGreaterThan:
|
||||
f.addWhere(fmt.Sprintf("%s > %d", widthHeight, max))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if codec != nil {
|
||||
if addJoinFn != nil {
|
||||
addJoinFn(f)
|
||||
}
|
||||
|
||||
stringCriterionHandler(codec, codecColumn)(ctx, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasMarkersCriterionHandler(hasMarkers *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if hasMarkers != nil {
|
||||
f.addLeftJoin("scene_markers", "", "scene_markers.scene_id = scenes.id")
|
||||
if *hasMarkers == "true" {
|
||||
f.addHaving("count(scene_markers.scene_id) > 0")
|
||||
} else {
|
||||
f.addWhere("scene_markers.id IS NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sceneIsMissingCriterionHandler(qb *SceneStore, isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "url":
|
||||
scenesURLsTableMgr.join(f, "", "scenes.id")
|
||||
f.addWhere("scene_urls.url IS NULL")
|
||||
case "galleries":
|
||||
qb.galleriesRepository().join(f, "galleries_join", "scenes.id")
|
||||
f.addWhere("galleries_join.scene_id IS NULL")
|
||||
case "studio":
|
||||
f.addWhere("scenes.studio_id IS NULL")
|
||||
case "movie":
|
||||
qb.moviesRepository().join(f, "movies_join", "scenes.id")
|
||||
f.addWhere("movies_join.scene_id IS NULL")
|
||||
case "performers":
|
||||
qb.performersRepository().join(f, "performers_join", "scenes.id")
|
||||
f.addWhere("performers_join.scene_id IS NULL")
|
||||
case "date":
|
||||
f.addWhere(`scenes.date IS NULL OR scenes.date IS ""`)
|
||||
case "tags":
|
||||
qb.tagsRepository().join(f, "tags_join", "scenes.id")
|
||||
f.addWhere("tags_join.scene_id IS NULL")
|
||||
case "stash_id":
|
||||
qb.stashIDRepository().join(f, "scene_stash_ids", "scenes.id")
|
||||
f.addWhere("scene_stash_ids.scene_id IS NULL")
|
||||
case "phash":
|
||||
qb.addSceneFilesTable(f)
|
||||
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
|
||||
f.addWhere("fingerprints_phash.fingerprint IS NULL")
|
||||
case "cover":
|
||||
f.addWhere("scenes.cover_blob IS NULL")
|
||||
default:
|
||||
f.addWhere("(scenes." + *isMissing + " IS NULL OR TRIM(scenes." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sceneURLsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: scenesURLsTable,
|
||||
stringColumn: sceneURLColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
scenesURLsTableMgr.join(f, "", "scenes.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(url)
|
||||
}
|
||||
|
||||
func (qb *SceneStore) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
|
||||
return multiCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
foreignTable: foreignTable,
|
||||
joinTable: joinTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
foreignFK: foreignFK,
|
||||
addJoinsFunc: addJoinsFunc,
|
||||
}
|
||||
}
|
||||
|
||||
func sceneCaptionCriterionHandler(qb *SceneStore, captions *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: videoCaptionsTable,
|
||||
stringColumn: captionCodeColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
qb.addSceneFilesTable(f)
|
||||
f.addLeftJoin(videoCaptionsTable, "", "video_captions.file_id = scenes_files.file_id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(captions)
|
||||
}
|
||||
|
||||
func sceneTagsCriterionHandler(qb *SceneStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedHierarchicalMultiCriterionHandlerBuilder{
|
||||
tx: qb.tx,
|
||||
|
||||
primaryTable: sceneTable,
|
||||
foreignTable: tagTable,
|
||||
foreignFK: "tag_id",
|
||||
|
||||
relationsTable: "tags_relations",
|
||||
joinAs: "scene_tag",
|
||||
joinTable: scenesTagsTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(tags)
|
||||
}
|
||||
|
||||
func sceneTagCountCriterionHandler(qb *SceneStore, tagCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
joinTable: scenesTagsTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(tagCount)
|
||||
}
|
||||
|
||||
func scenePerformersCriterionHandler(qb *SceneStore, performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedMultiCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
joinTable: performersScenesTable,
|
||||
joinAs: "performers_join",
|
||||
primaryFK: sceneIDColumn,
|
||||
foreignFK: performerIDColumn,
|
||||
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
qb.performersRepository().join(f, "performers_join", "scenes.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(performers)
|
||||
}
|
||||
|
||||
func scenePerformerCountCriterionHandler(qb *SceneStore, performerCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
joinTable: performersScenesTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(performerCount)
|
||||
}
|
||||
|
||||
func scenePerformerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performerfavorite != nil {
|
||||
f.addLeftJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id")
|
||||
|
||||
if *performerfavorite {
|
||||
// contains at least one favorite
|
||||
f.addLeftJoin("performers", "", "performers.id = performers_scenes.performer_id")
|
||||
f.addWhere("performers.favorite = 1")
|
||||
} else {
|
||||
// contains zero favorites
|
||||
f.addLeftJoin(`(SELECT performers_scenes.scene_id as id FROM performers_scenes
|
||||
JOIN performers ON performers.id = performers_scenes.performer_id
|
||||
GROUP BY performers_scenes.scene_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "scenes.id = nofaves.id")
|
||||
f.addWhere("performers_scenes.scene_id IS NULL OR nofaves.id IS NOT NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func scenePerformerAgeCriterionHandler(performerAge *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performerAge != nil {
|
||||
f.addInnerJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id")
|
||||
f.addInnerJoin("performers", "", "performers_scenes.performer_id = performers.id")
|
||||
|
||||
f.addWhere("scenes.date != '' AND performers.birthdate != ''")
|
||||
f.addWhere("scenes.date IS NOT NULL AND performers.birthdate IS NOT NULL")
|
||||
|
||||
ageCalc := "cast(strftime('%Y.%m%d', scenes.date) - strftime('%Y.%m%d', performers.birthdate) as int)"
|
||||
whereClause, args := getIntWhereClause(ageCalc, performerAge.Modifier, performerAge.Value, performerAge.Value2)
|
||||
f.addWhere(whereClause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sceneMoviesCriterionHandler(qb *SceneStore, movies *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
qb.moviesRepository().join(f, "", "scenes.id")
|
||||
f.addLeftJoin("movies", "", "movies_scenes.movie_id = movies.id")
|
||||
}
|
||||
h := qb.getMultiCriterionHandlerBuilder(movieTable, moviesScenesTable, "movie_id", addJoinsFunc)
|
||||
return h.handler(movies)
|
||||
}
|
||||
|
||||
func sceneGalleriesCriterionHandler(qb *SceneStore, galleries *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
qb.galleriesRepository().join(f, "", "scenes.id")
|
||||
f.addLeftJoin("galleries", "", "scenes_galleries.gallery_id = galleries.id")
|
||||
}
|
||||
h := qb.getMultiCriterionHandlerBuilder(galleryTable, scenesGalleriesTable, "gallery_id", addJoinsFunc)
|
||||
return h.handler(galleries)
|
||||
}
|
||||
|
||||
func scenePerformerTagsCriterionHandler(qb *SceneStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
|
||||
return &joinedPerformerTagsHandler{
|
||||
criterion: tags,
|
||||
primaryTable: sceneTable,
|
||||
joinTable: performersScenesTable,
|
||||
joinPrimaryKey: sceneIDColumn,
|
||||
}
|
||||
}
|
||||
|
||||
func scenePhashDistanceCriterionHandler(qb *SceneStore, phashDistance *models.PhashDistanceCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if phashDistance != nil {
|
||||
qb.addSceneFilesTable(f)
|
||||
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
|
||||
|
||||
value, _ := utils.StringToPhash(phashDistance.Value)
|
||||
distance := 0
|
||||
if phashDistance.Distance != nil {
|
||||
distance = *phashDistance.Distance
|
||||
}
|
||||
|
||||
if distance == 0 {
|
||||
// use the default handler
|
||||
intCriterionHandler(&models.IntCriterionInput{
|
||||
Value: int(value),
|
||||
Modifier: phashDistance.Modifier,
|
||||
}, "fingerprints_phash.fingerprint", nil)(ctx, f)
|
||||
}
|
||||
|
||||
switch {
|
||||
case phashDistance.Modifier == models.CriterionModifierEquals && distance > 0:
|
||||
// needed to avoid a type mismatch
|
||||
f.addWhere("typeof(fingerprints_phash.fingerprint) = 'integer'")
|
||||
f.addWhere("phash_distance(fingerprints_phash.fingerprint, ?) < ?", value, distance)
|
||||
case phashDistance.Modifier == models.CriterionModifierNotEquals && distance > 0:
|
||||
// needed to avoid a type mismatch
|
||||
f.addWhere("typeof(fingerprints_phash.fingerprint) = 'integer'")
|
||||
f.addWhere("phash_distance(fingerprints_phash.fingerprint, ?) > ?", value, distance)
|
||||
default:
|
||||
intCriterionHandler(&models.IntCriterionInput{
|
||||
Value: int(value),
|
||||
Modifier: phashDistance.Modifier,
|
||||
}, "fingerprints_phash.fingerprint", nil)(ctx, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sceneSortOptions = sortOptions{
|
||||
"bitrate",
|
||||
"created_at",
|
||||
@@ -1719,7 +1260,7 @@ func (qb *SceneStore) AssignFiles(ctx context.Context, sceneID int, fileIDs []mo
|
||||
}
|
||||
|
||||
// assign primary only if destination has no files
|
||||
existingFileIDs, err := qb.filesRepository().get(ctx, sceneID)
|
||||
existingFileIDs, err := sceneRepository.files.get(ctx, sceneID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1728,18 +1269,10 @@ func (qb *SceneStore) AssignFiles(ctx context.Context, sceneID int, fileIDs []mo
|
||||
return scenesFilesTableMgr.insertJoins(ctx, sceneID, firstPrimary, fileIDs)
|
||||
}
|
||||
|
||||
func (qb *SceneStore) moviesRepository() *repository {
|
||||
return &repository{
|
||||
tx: qb.tx,
|
||||
tableName: moviesScenesTable,
|
||||
idColumn: sceneIDColumn,
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *SceneStore) GetMovies(ctx context.Context, id int) (ret []models.MoviesScenes, err error) {
|
||||
ret = []models.MoviesScenes{}
|
||||
|
||||
if err := qb.moviesRepository().getAll(ctx, id, func(rows *sqlx.Rows) error {
|
||||
if err := sceneRepository.movies.getAll(ctx, id, func(rows *sqlx.Rows) error {
|
||||
var ms moviesScenesRow
|
||||
if err := rows.StructScan(&ms); err != nil {
|
||||
return err
|
||||
@@ -1754,91 +1287,36 @@ func (qb *SceneStore) GetMovies(ctx context.Context, id int) (ret []models.Movie
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (qb *SceneStore) filesRepository() *filesRepository {
|
||||
return &filesRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: scenesFilesTable,
|
||||
idColumn: sceneIDColumn,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *SceneStore) AddFileID(ctx context.Context, id int, fileID models.FileID) error {
|
||||
const firstPrimary = false
|
||||
return scenesFilesTableMgr.insertJoins(ctx, id, firstPrimary, []models.FileID{fileID})
|
||||
}
|
||||
|
||||
func (qb *SceneStore) performersRepository() *joinRepository {
|
||||
return &joinRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: performersScenesTable,
|
||||
idColumn: sceneIDColumn,
|
||||
},
|
||||
fkColumn: performerIDColumn,
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *SceneStore) GetPerformerIDs(ctx context.Context, id int) ([]int, error) {
|
||||
return qb.performersRepository().getIDs(ctx, id)
|
||||
}
|
||||
|
||||
func (qb *SceneStore) tagsRepository() *joinRepository {
|
||||
return &joinRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: scenesTagsTable,
|
||||
idColumn: sceneIDColumn,
|
||||
},
|
||||
fkColumn: tagIDColumn,
|
||||
foreignTable: tagTable,
|
||||
orderBy: "tags.name ASC",
|
||||
}
|
||||
return sceneRepository.performers.getIDs(ctx, id)
|
||||
}
|
||||
|
||||
func (qb *SceneStore) GetTagIDs(ctx context.Context, id int) ([]int, error) {
|
||||
return qb.tagsRepository().getIDs(ctx, id)
|
||||
}
|
||||
|
||||
func (qb *SceneStore) galleriesRepository() *joinRepository {
|
||||
return &joinRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: scenesGalleriesTable,
|
||||
idColumn: sceneIDColumn,
|
||||
},
|
||||
fkColumn: galleryIDColumn,
|
||||
}
|
||||
return sceneRepository.tags.getIDs(ctx, id)
|
||||
}
|
||||
|
||||
func (qb *SceneStore) GetGalleryIDs(ctx context.Context, id int) ([]int, error) {
|
||||
return qb.galleriesRepository().getIDs(ctx, id)
|
||||
return sceneRepository.galleries.getIDs(ctx, id)
|
||||
}
|
||||
|
||||
func (qb *SceneStore) AddGalleryIDs(ctx context.Context, sceneID int, galleryIDs []int) error {
|
||||
return scenesGalleriesTableMgr.addJoins(ctx, sceneID, galleryIDs)
|
||||
}
|
||||
|
||||
func (qb *SceneStore) stashIDRepository() *stashIDRepository {
|
||||
return &stashIDRepository{
|
||||
repository{
|
||||
tx: qb.tx,
|
||||
tableName: "scene_stash_ids",
|
||||
idColumn: sceneIDColumn,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *SceneStore) GetStashIDs(ctx context.Context, sceneID int) ([]models.StashID, error) {
|
||||
return qb.stashIDRepository().get(ctx, sceneID)
|
||||
return sceneRepository.stashIDs.get(ctx, sceneID)
|
||||
}
|
||||
|
||||
func (qb *SceneStore) FindDuplicates(ctx context.Context, distance int, durationDiff float64) ([][]*models.Scene, error) {
|
||||
var dupeIds [][]int
|
||||
if distance == 0 {
|
||||
var ids []string
|
||||
if err := qb.tx.Select(ctx, &ids, findExactDuplicateQuery, durationDiff); err != nil {
|
||||
if err := dbWrapper.Select(ctx, &ids, findExactDuplicateQuery, durationDiff); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1858,7 +1336,7 @@ func (qb *SceneStore) FindDuplicates(ctx context.Context, distance int, duration
|
||||
} else {
|
||||
var hashes []*utils.Phash
|
||||
|
||||
if err := qb.queryFunc(ctx, findAllPhashesQuery, nil, false, func(rows *sqlx.Rows) error {
|
||||
if err := sceneRepository.queryFunc(ctx, findAllPhashesQuery, nil, false, func(rows *sqlx.Rows) error {
|
||||
phash := utils.Phash{
|
||||
Bucket: -1,
|
||||
Duration: -1,
|
||||
|
||||
533
pkg/sqlite/scene_filter.go
Normal file
533
pkg/sqlite/scene_filter.go
Normal file
@@ -0,0 +1,533 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type sceneFilterHandler struct {
|
||||
sceneFilter *models.SceneFilterType
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) validate() error {
|
||||
sceneFilter := qb.sceneFilter
|
||||
if sceneFilter == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := validateFilterCombination(sceneFilter.OperatorFilter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if subFilter := sceneFilter.SubFilter(); subFilter != nil {
|
||||
sqb := &sceneFilterHandler{sceneFilter: subFilter}
|
||||
if err := sqb.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) handle(ctx context.Context, f *filterBuilder) {
|
||||
sceneFilter := qb.sceneFilter
|
||||
if sceneFilter == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := qb.validate(); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
sf := sceneFilter.SubFilter()
|
||||
if sf != nil {
|
||||
sub := &sceneFilterHandler{sf}
|
||||
handleSubFilter(ctx, sub, f, sceneFilter.OperatorFilter)
|
||||
}
|
||||
|
||||
f.handleCriterion(ctx, qb.criterionHandler())
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
|
||||
sceneFilter := qb.sceneFilter
|
||||
return compoundHandler{
|
||||
intCriterionHandler(sceneFilter.ID, "scenes.id", nil),
|
||||
pathCriterionHandler(sceneFilter.Path, "folders.path", "files.basename", qb.addFoldersTable),
|
||||
qb.fileCountCriterionHandler(sceneFilter.FileCount),
|
||||
stringCriterionHandler(sceneFilter.Title, "scenes.title"),
|
||||
stringCriterionHandler(sceneFilter.Code, "scenes.code"),
|
||||
stringCriterionHandler(sceneFilter.Details, "scenes.details"),
|
||||
stringCriterionHandler(sceneFilter.Director, "scenes.director"),
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if sceneFilter.Oshash != nil {
|
||||
qb.addSceneFilesTable(f)
|
||||
f.addLeftJoin(fingerprintTable, "fingerprints_oshash", "scenes_files.file_id = fingerprints_oshash.file_id AND fingerprints_oshash.type = 'oshash'")
|
||||
}
|
||||
|
||||
stringCriterionHandler(sceneFilter.Oshash, "fingerprints_oshash.fingerprint")(ctx, f)
|
||||
}),
|
||||
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if sceneFilter.Checksum != nil {
|
||||
qb.addSceneFilesTable(f)
|
||||
f.addLeftJoin(fingerprintTable, "fingerprints_md5", "scenes_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
|
||||
}
|
||||
|
||||
stringCriterionHandler(sceneFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
|
||||
}),
|
||||
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if sceneFilter.Phash != nil {
|
||||
// backwards compatibility
|
||||
qb.phashDistanceCriterionHandler(&models.PhashDistanceCriterionInput{
|
||||
Value: sceneFilter.Phash.Value,
|
||||
Modifier: sceneFilter.Phash.Modifier,
|
||||
})(ctx, f)
|
||||
}
|
||||
}),
|
||||
|
||||
qb.phashDistanceCriterionHandler(sceneFilter.PhashDistance),
|
||||
|
||||
intCriterionHandler(sceneFilter.Rating100, "scenes.rating", nil),
|
||||
qb.oCountCriterionHandler(sceneFilter.OCounter),
|
||||
boolCriterionHandler(sceneFilter.Organized, "scenes.organized", nil),
|
||||
|
||||
floatIntCriterionHandler(sceneFilter.Duration, "video_files.duration", qb.addVideoFilesTable),
|
||||
resolutionCriterionHandler(sceneFilter.Resolution, "video_files.height", "video_files.width", qb.addVideoFilesTable),
|
||||
orientationCriterionHandler(sceneFilter.Orientation, "video_files.height", "video_files.width", qb.addVideoFilesTable),
|
||||
floatIntCriterionHandler(sceneFilter.Framerate, "ROUND(video_files.frame_rate)", qb.addVideoFilesTable),
|
||||
intCriterionHandler(sceneFilter.Bitrate, "video_files.bit_rate", qb.addVideoFilesTable),
|
||||
qb.codecCriterionHandler(sceneFilter.VideoCodec, "video_files.video_codec", qb.addVideoFilesTable),
|
||||
qb.codecCriterionHandler(sceneFilter.AudioCodec, "video_files.audio_codec", qb.addVideoFilesTable),
|
||||
|
||||
qb.hasMarkersCriterionHandler(sceneFilter.HasMarkers),
|
||||
qb.isMissingCriterionHandler(sceneFilter.IsMissing),
|
||||
qb.urlsCriterionHandler(sceneFilter.URL),
|
||||
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if sceneFilter.StashID != nil {
|
||||
sceneRepository.stashIDs.join(f, "scene_stash_ids", "scenes.id")
|
||||
stringCriterionHandler(sceneFilter.StashID, "scene_stash_ids.stash_id")(ctx, f)
|
||||
}
|
||||
}),
|
||||
|
||||
&stashIDCriterionHandler{
|
||||
c: sceneFilter.StashIDEndpoint,
|
||||
stashIDRepository: &sceneRepository.stashIDs,
|
||||
stashIDTableAs: "scene_stash_ids",
|
||||
parentIDCol: "scenes.id",
|
||||
},
|
||||
|
||||
boolCriterionHandler(sceneFilter.Interactive, "video_files.interactive", qb.addVideoFilesTable),
|
||||
intCriterionHandler(sceneFilter.InteractiveSpeed, "video_files.interactive_speed", qb.addVideoFilesTable),
|
||||
|
||||
qb.captionCriterionHandler(sceneFilter.Captions),
|
||||
|
||||
floatIntCriterionHandler(sceneFilter.ResumeTime, "scenes.resume_time", nil),
|
||||
floatIntCriterionHandler(sceneFilter.PlayDuration, "scenes.play_duration", nil),
|
||||
qb.playCountCriterionHandler(sceneFilter.PlayCount),
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if sceneFilter.LastPlayedAt != nil {
|
||||
f.addLeftJoin(
|
||||
fmt.Sprintf("(SELECT %s, MAX(%s) as last_played_at FROM %s GROUP BY %s)", sceneIDColumn, sceneViewDateColumn, scenesViewDatesTable, sceneIDColumn),
|
||||
"scene_last_view",
|
||||
fmt.Sprintf("scene_last_view.%s = scenes.id", sceneIDColumn),
|
||||
)
|
||||
h := timestampCriterionHandler{sceneFilter.LastPlayedAt, "IFNULL(last_played_at, datetime(0))", nil}
|
||||
h.handle(ctx, f)
|
||||
}
|
||||
}),
|
||||
|
||||
qb.tagsCriterionHandler(sceneFilter.Tags),
|
||||
qb.tagCountCriterionHandler(sceneFilter.TagCount),
|
||||
qb.performersCriterionHandler(sceneFilter.Performers),
|
||||
qb.performerCountCriterionHandler(sceneFilter.PerformerCount),
|
||||
studioCriterionHandler(sceneTable, sceneFilter.Studios),
|
||||
qb.moviesCriterionHandler(sceneFilter.Movies),
|
||||
qb.galleriesCriterionHandler(sceneFilter.Galleries),
|
||||
qb.performerTagsCriterionHandler(sceneFilter.PerformerTags),
|
||||
qb.performerFavoriteCriterionHandler(sceneFilter.PerformerFavorite),
|
||||
qb.performerAgeCriterionHandler(sceneFilter.PerformerAge),
|
||||
qb.phashDuplicatedCriterionHandler(sceneFilter.Duplicated, qb.addSceneFilesTable),
|
||||
&dateCriterionHandler{sceneFilter.Date, "scenes.date", nil},
|
||||
×tampCriterionHandler{sceneFilter.CreatedAt, "scenes.created_at", nil},
|
||||
×tampCriterionHandler{sceneFilter.UpdatedAt, "scenes.updated_at", nil},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "scenes_galleries.gallery_id",
|
||||
relatedRepo: galleryRepository.repository,
|
||||
relatedHandler: &galleryFilterHandler{sceneFilter.GalleriesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
sceneRepository.galleries.innerJoin(f, "", "scenes.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "performers_join.performer_id",
|
||||
relatedRepo: performerRepository.repository,
|
||||
relatedHandler: &performerFilterHandler{sceneFilter.PerformersFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
sceneRepository.performers.innerJoin(f, "performers_join", "scenes.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "scenes.studio_id",
|
||||
relatedRepo: studioRepository.repository,
|
||||
relatedHandler: &studioFilterHandler{sceneFilter.StudiosFilter},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "scene_tag.tag_id",
|
||||
relatedRepo: tagRepository.repository,
|
||||
relatedHandler: &tagFilterHandler{sceneFilter.TagsFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
sceneRepository.tags.innerJoin(f, "scene_tag", "scenes.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "movies_scenes.movie_id",
|
||||
relatedRepo: movieRepository.repository,
|
||||
relatedHandler: &movieFilterHandler{sceneFilter.MoviesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
sceneRepository.movies.innerJoin(f, "", "scenes.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "scene_markers.id",
|
||||
relatedRepo: sceneMarkerRepository.repository,
|
||||
relatedHandler: &sceneMarkerFilterHandler{sceneFilter.MarkersFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
f.addInnerJoin("scene_markers", "", "scenes.id")
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) addSceneFilesTable(f *filterBuilder) {
|
||||
f.addLeftJoin(scenesFilesTable, "", "scenes_files.scene_id = scenes.id")
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) addFilesTable(f *filterBuilder) {
|
||||
qb.addSceneFilesTable(f)
|
||||
f.addLeftJoin(fileTable, "", "scenes_files.file_id = files.id")
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) addFoldersTable(f *filterBuilder) {
|
||||
qb.addFilesTable(f)
|
||||
f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id")
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) addVideoFilesTable(f *filterBuilder) {
|
||||
qb.addSceneFilesTable(f)
|
||||
f.addLeftJoin(videoFileTable, "", "video_files.file_id = scenes_files.file_id")
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) playCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
joinTable: scenesViewDatesTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(count)
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) oCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
joinTable: scenesODatesTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(count)
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) fileCountCriterionHandler(fileCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
joinTable: scenesFilesTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(fileCount)
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) phashDuplicatedCriterionHandler(duplicatedFilter *models.PHashDuplicationCriterionInput, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
// TODO: Wishlist item: Implement Distance matching
|
||||
if duplicatedFilter != nil {
|
||||
if addJoinFn != nil {
|
||||
addJoinFn(f)
|
||||
}
|
||||
|
||||
var v string
|
||||
if *duplicatedFilter.Duplicated {
|
||||
v = ">"
|
||||
} else {
|
||||
v = "="
|
||||
}
|
||||
|
||||
f.addInnerJoin("(SELECT file_id FROM files_fingerprints INNER JOIN (SELECT fingerprint FROM files_fingerprints WHERE type = 'phash' GROUP BY fingerprint HAVING COUNT (fingerprint) "+v+" 1) dupes on files_fingerprints.fingerprint = dupes.fingerprint)", "scph", "scenes_files.file_id = scph.file_id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if codec != nil {
|
||||
if addJoinFn != nil {
|
||||
addJoinFn(f)
|
||||
}
|
||||
|
||||
stringCriterionHandler(codec, codecColumn)(ctx, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) hasMarkersCriterionHandler(hasMarkers *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if hasMarkers != nil {
|
||||
f.addLeftJoin("scene_markers", "", "scene_markers.scene_id = scenes.id")
|
||||
if *hasMarkers == "true" {
|
||||
f.addHaving("count(scene_markers.scene_id) > 0")
|
||||
} else {
|
||||
f.addWhere("scene_markers.id IS NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) isMissingCriterionHandler(isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "url":
|
||||
scenesURLsTableMgr.join(f, "", "scenes.id")
|
||||
f.addWhere("scene_urls.url IS NULL")
|
||||
case "galleries":
|
||||
sceneRepository.galleries.join(f, "galleries_join", "scenes.id")
|
||||
f.addWhere("galleries_join.scene_id IS NULL")
|
||||
case "studio":
|
||||
f.addWhere("scenes.studio_id IS NULL")
|
||||
case "movie":
|
||||
sceneRepository.movies.join(f, "movies_join", "scenes.id")
|
||||
f.addWhere("movies_join.scene_id IS NULL")
|
||||
case "performers":
|
||||
sceneRepository.performers.join(f, "performers_join", "scenes.id")
|
||||
f.addWhere("performers_join.scene_id IS NULL")
|
||||
case "date":
|
||||
f.addWhere(`scenes.date IS NULL OR scenes.date IS ""`)
|
||||
case "tags":
|
||||
sceneRepository.tags.join(f, "tags_join", "scenes.id")
|
||||
f.addWhere("tags_join.scene_id IS NULL")
|
||||
case "stash_id":
|
||||
sceneRepository.stashIDs.join(f, "scene_stash_ids", "scenes.id")
|
||||
f.addWhere("scene_stash_ids.scene_id IS NULL")
|
||||
case "phash":
|
||||
qb.addSceneFilesTable(f)
|
||||
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
|
||||
f.addWhere("fingerprints_phash.fingerprint IS NULL")
|
||||
case "cover":
|
||||
f.addWhere("scenes.cover_blob IS NULL")
|
||||
default:
|
||||
f.addWhere("(scenes." + *isMissing + " IS NULL OR TRIM(scenes." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) urlsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: scenesURLsTable,
|
||||
stringColumn: sceneURLColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
scenesURLsTableMgr.join(f, "", "scenes.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(url)
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
|
||||
return multiCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
foreignTable: foreignTable,
|
||||
joinTable: joinTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
foreignFK: foreignFK,
|
||||
addJoinsFunc: addJoinsFunc,
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) captionCriterionHandler(captions *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: videoCaptionsTable,
|
||||
stringColumn: captionCodeColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
qb.addSceneFilesTable(f)
|
||||
f.addLeftJoin(videoCaptionsTable, "", "video_captions.file_id = scenes_files.file_id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(captions)
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) tagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedHierarchicalMultiCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
foreignTable: tagTable,
|
||||
foreignFK: "tag_id",
|
||||
|
||||
relationsTable: "tags_relations",
|
||||
joinAs: "scene_tag",
|
||||
joinTable: scenesTagsTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(tags)
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) tagCountCriterionHandler(tagCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
joinTable: scenesTagsTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(tagCount)
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) performersCriterionHandler(performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedMultiCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
joinTable: performersScenesTable,
|
||||
joinAs: "performers_join",
|
||||
primaryFK: sceneIDColumn,
|
||||
foreignFK: performerIDColumn,
|
||||
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
sceneRepository.performers.join(f, "performers_join", "scenes.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(performers)
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) performerCountCriterionHandler(performerCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
joinTable: performersScenesTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
}
|
||||
|
||||
return h.handler(performerCount)
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) performerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performerfavorite != nil {
|
||||
f.addLeftJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id")
|
||||
|
||||
if *performerfavorite {
|
||||
// contains at least one favorite
|
||||
f.addLeftJoin("performers", "", "performers.id = performers_scenes.performer_id")
|
||||
f.addWhere("performers.favorite = 1")
|
||||
} else {
|
||||
// contains zero favorites
|
||||
f.addLeftJoin(`(SELECT performers_scenes.scene_id as id FROM performers_scenes
|
||||
JOIN performers ON performers.id = performers_scenes.performer_id
|
||||
GROUP BY performers_scenes.scene_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "scenes.id = nofaves.id")
|
||||
f.addWhere("performers_scenes.scene_id IS NULL OR nofaves.id IS NOT NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) performerAgeCriterionHandler(performerAge *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performerAge != nil {
|
||||
f.addInnerJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id")
|
||||
f.addInnerJoin("performers", "", "performers_scenes.performer_id = performers.id")
|
||||
|
||||
f.addWhere("scenes.date != '' AND performers.birthdate != ''")
|
||||
f.addWhere("scenes.date IS NOT NULL AND performers.birthdate IS NOT NULL")
|
||||
|
||||
ageCalc := "cast(strftime('%Y.%m%d', scenes.date) - strftime('%Y.%m%d', performers.birthdate) as int)"
|
||||
whereClause, args := getIntWhereClause(ageCalc, performerAge.Modifier, performerAge.Value, performerAge.Value2)
|
||||
f.addWhere(whereClause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) moviesCriterionHandler(movies *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
sceneRepository.movies.join(f, "", "scenes.id")
|
||||
f.addLeftJoin("movies", "", "movies_scenes.movie_id = movies.id")
|
||||
}
|
||||
h := qb.getMultiCriterionHandlerBuilder(movieTable, moviesScenesTable, "movie_id", addJoinsFunc)
|
||||
return h.handler(movies)
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) galleriesCriterionHandler(galleries *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
sceneRepository.galleries.join(f, "", "scenes.id")
|
||||
f.addLeftJoin("galleries", "", "scenes_galleries.gallery_id = galleries.id")
|
||||
}
|
||||
h := qb.getMultiCriterionHandlerBuilder(galleryTable, scenesGalleriesTable, "gallery_id", addJoinsFunc)
|
||||
return h.handler(galleries)
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) performerTagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandler {
|
||||
return &joinedPerformerTagsHandler{
|
||||
criterion: tags,
|
||||
primaryTable: sceneTable,
|
||||
joinTable: performersScenesTable,
|
||||
joinPrimaryKey: sceneIDColumn,
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) phashDistanceCriterionHandler(phashDistance *models.PhashDistanceCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if phashDistance != nil {
|
||||
qb.addSceneFilesTable(f)
|
||||
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
|
||||
|
||||
value, _ := utils.StringToPhash(phashDistance.Value)
|
||||
distance := 0
|
||||
if phashDistance.Distance != nil {
|
||||
distance = *phashDistance.Distance
|
||||
}
|
||||
|
||||
if distance == 0 {
|
||||
// use the default handler
|
||||
intCriterionHandler(&models.IntCriterionInput{
|
||||
Value: int(value),
|
||||
Modifier: phashDistance.Modifier,
|
||||
}, "fingerprints_phash.fingerprint", nil)(ctx, f)
|
||||
}
|
||||
|
||||
switch {
|
||||
case phashDistance.Modifier == models.CriterionModifierEquals && distance > 0:
|
||||
// needed to avoid a type mismatch
|
||||
f.addWhere("typeof(fingerprints_phash.fingerprint) = 'integer'")
|
||||
f.addWhere("phash_distance(fingerprints_phash.fingerprint, ?) < ?", value, distance)
|
||||
case phashDistance.Modifier == models.CriterionModifierNotEquals && distance > 0:
|
||||
// needed to avoid a type mismatch
|
||||
f.addWhere("typeof(fingerprints_phash.fingerprint) = 'integer'")
|
||||
f.addWhere("phash_distance(fingerprints_phash.fingerprint, ?) > ?", value, distance)
|
||||
default:
|
||||
intCriterionHandler(&models.IntCriterionInput{
|
||||
Value: int(value),
|
||||
Modifier: phashDistance.Modifier,
|
||||
}, "fingerprints_phash.fingerprint", nil)(ctx, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,24 +75,41 @@ func (r *sceneMarkerRowRecord) fromPartial(o models.SceneMarkerPartial) {
|
||||
r.setTimestamp("updated_at", o.UpdatedAt)
|
||||
}
|
||||
|
||||
type SceneMarkerStore struct {
|
||||
type sceneMarkerRepositoryType struct {
|
||||
repository
|
||||
|
||||
tableMgr *table
|
||||
scenes repository
|
||||
tags joinRepository
|
||||
}
|
||||
|
||||
func NewSceneMarkerStore() *SceneMarkerStore {
|
||||
return &SceneMarkerStore{
|
||||
var (
|
||||
sceneMarkerRepository = sceneMarkerRepositoryType{
|
||||
repository: repository{
|
||||
tableName: sceneMarkerTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
tableMgr: sceneMarkerTableMgr,
|
||||
scenes: repository{
|
||||
tableName: sceneTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
tags: joinRepository{
|
||||
repository: repository{
|
||||
tableName: "scene_markers_tags",
|
||||
idColumn: "scene_marker_id",
|
||||
},
|
||||
fkColumn: tagIDColumn,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type SceneMarkerStore struct{}
|
||||
|
||||
func NewSceneMarkerStore() *SceneMarkerStore {
|
||||
return &SceneMarkerStore{}
|
||||
}
|
||||
|
||||
func (qb *SceneMarkerStore) table() exp.IdentifierExpression {
|
||||
return qb.tableMgr.table
|
||||
return sceneMarkerTableMgr.table
|
||||
}
|
||||
|
||||
func (qb *SceneMarkerStore) selectDataset() *goqu.SelectDataset {
|
||||
@@ -103,7 +120,7 @@ func (qb *SceneMarkerStore) Create(ctx context.Context, newObject *models.SceneM
|
||||
var r sceneMarkerRow
|
||||
r.fromSceneMarker(*newObject)
|
||||
|
||||
id, err := qb.tableMgr.insertID(ctx, r)
|
||||
id, err := sceneMarkerTableMgr.insertID(ctx, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -128,7 +145,7 @@ func (qb *SceneMarkerStore) UpdatePartial(ctx context.Context, id int, partial m
|
||||
r.fromPartial(partial)
|
||||
|
||||
if len(r.Record) > 0 {
|
||||
if err := qb.tableMgr.updateByID(ctx, id, r.Record); err != nil {
|
||||
if err := sceneMarkerTableMgr.updateByID(ctx, id, r.Record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -140,7 +157,7 @@ func (qb *SceneMarkerStore) Update(ctx context.Context, updatedObject *models.Sc
|
||||
var r sceneMarkerRow
|
||||
r.fromSceneMarker(*updatedObject)
|
||||
|
||||
if err := qb.tableMgr.updateByID(ctx, updatedObject.ID, r); err != nil {
|
||||
if err := sceneMarkerTableMgr.updateByID(ctx, updatedObject.ID, r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -148,7 +165,7 @@ func (qb *SceneMarkerStore) Update(ctx context.Context, updatedObject *models.Sc
|
||||
}
|
||||
|
||||
func (qb *SceneMarkerStore) Destroy(ctx context.Context, id int) error {
|
||||
return qb.destroyExisting(ctx, []int{id})
|
||||
return sceneMarkerRepository.destroyExisting(ctx, []int{id})
|
||||
}
|
||||
|
||||
// returns nil, nil if not found
|
||||
@@ -186,7 +203,7 @@ func (qb *SceneMarkerStore) FindMany(ctx context.Context, ids []int) ([]*models.
|
||||
|
||||
// returns nil, sql.ErrNoRows if not found
|
||||
func (qb *SceneMarkerStore) find(ctx context.Context, id int) (*models.SceneMarker, error) {
|
||||
q := qb.selectDataset().Where(qb.tableMgr.byID(id))
|
||||
q := qb.selectDataset().Where(sceneMarkerTableMgr.byID(id))
|
||||
|
||||
ret, err := qb.get(ctx, q)
|
||||
if err != nil {
|
||||
@@ -243,7 +260,7 @@ func (qb *SceneMarkerStore) FindBySceneID(ctx context.Context, sceneID int) ([]*
|
||||
|
||||
func (qb *SceneMarkerStore) CountByTagID(ctx context.Context, tagID int) (int, error) {
|
||||
args := []interface{}{tagID, tagID}
|
||||
return qb.runCountQuery(ctx, qb.buildCountQuery(countSceneMarkersForTagQuery), args)
|
||||
return sceneMarkerRepository.runCountQuery(ctx, sceneMarkerRepository.buildCountQuery(countSceneMarkersForTagQuery), args)
|
||||
}
|
||||
|
||||
func (qb *SceneMarkerStore) GetMarkerStrings(ctx context.Context, q *string, sort *string) ([]*models.MarkerStringsResultType, error) {
|
||||
@@ -272,21 +289,6 @@ func (qb *SceneMarkerStore) Wall(ctx context.Context, q *string) ([]*models.Scen
|
||||
return qb.getMany(ctx, qq)
|
||||
}
|
||||
|
||||
func (qb *SceneMarkerStore) makeFilter(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType) *filterBuilder {
|
||||
query := &filterBuilder{}
|
||||
|
||||
query.handleCriterion(ctx, sceneMarkerTagIDCriterionHandler(qb, sceneMarkerFilter.TagID))
|
||||
query.handleCriterion(ctx, sceneMarkerTagsCriterionHandler(qb, sceneMarkerFilter.Tags))
|
||||
query.handleCriterion(ctx, sceneMarkerSceneTagsCriterionHandler(qb, sceneMarkerFilter.SceneTags))
|
||||
query.handleCriterion(ctx, sceneMarkerPerformersCriterionHandler(qb, sceneMarkerFilter.Performers))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(sceneMarkerFilter.CreatedAt, "scene_markers.created_at"))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(sceneMarkerFilter.UpdatedAt, "scene_markers.updated_at"))
|
||||
query.handleCriterion(ctx, dateCriterionHandler(sceneMarkerFilter.SceneDate, "scenes.date"))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(sceneMarkerFilter.SceneCreatedAt, "scenes.created_at"))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(sceneMarkerFilter.SceneUpdatedAt, "scenes.updated_at"))
|
||||
|
||||
return query
|
||||
}
|
||||
func (qb *SceneMarkerStore) makeQuery(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
|
||||
if sceneMarkerFilter == nil {
|
||||
sceneMarkerFilter = &models.SceneMarkerFilterType{}
|
||||
@@ -295,7 +297,7 @@ func (qb *SceneMarkerStore) makeQuery(ctx context.Context, sceneMarkerFilter *mo
|
||||
findFilter = &models.FindFilterType{}
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
query := sceneMarkerRepository.newQuery()
|
||||
distinctIDs(&query, sceneMarkerTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
@@ -304,7 +306,9 @@ func (qb *SceneMarkerStore) makeQuery(ctx context.Context, sceneMarkerFilter *mo
|
||||
query.parseQueryString(searchColumns, *q)
|
||||
}
|
||||
|
||||
filter := qb.makeFilter(ctx, sceneMarkerFilter)
|
||||
filter := filterBuilderFromHandler(ctx, &sceneMarkerFilterHandler{
|
||||
sceneMarkerFilter: sceneMarkerFilter,
|
||||
})
|
||||
|
||||
if err := query.addFilter(filter); err != nil {
|
||||
return nil, err
|
||||
@@ -346,135 +350,6 @@ func (qb *SceneMarkerStore) QueryCount(ctx context.Context, sceneMarkerFilter *m
|
||||
return query.executeCount(ctx)
|
||||
}
|
||||
|
||||
func sceneMarkerTagIDCriterionHandler(qb *SceneMarkerStore, tagID *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if tagID != nil {
|
||||
f.addLeftJoin("scene_markers_tags", "", "scene_markers_tags.scene_marker_id = scene_markers.id")
|
||||
|
||||
f.addWhere("(scene_markers.primary_tag_id = ? OR scene_markers_tags.tag_id = ?)", *tagID, *tagID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sceneMarkerTagsCriterionHandler(qb *SceneMarkerStore, criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if criterion != nil {
|
||||
tags := criterion.CombineExcludes()
|
||||
|
||||
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if tags.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
f.addLeftJoin("scene_markers_tags", "", "scene_markers.id = scene_markers_tags.scene_marker_id")
|
||||
|
||||
f.addWhere(fmt.Sprintf("%s scene_markers_tags.tag_id IS NULL", notClause))
|
||||
return
|
||||
}
|
||||
|
||||
if tags.Modifier == models.CriterionModifierEquals && tags.Depth != nil && *tags.Depth != 0 {
|
||||
f.setError(fmt.Errorf("depth is not supported for equals modifier for marker tag filtering"))
|
||||
return
|
||||
}
|
||||
|
||||
if len(tags.Value) == 0 && len(tags.Excludes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(tags.Value) > 0 {
|
||||
valuesClause, err := getHierarchicalValues(ctx, qb.tx, tags.Value, tagTable, "tags_relations", "parent_id", "child_id", tags.Depth)
|
||||
if err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
f.addWith(`marker_tags AS (
|
||||
SELECT mt.scene_marker_id, t.column1 AS root_tag_id FROM scene_markers_tags mt
|
||||
INNER JOIN (` + valuesClause + `) t ON t.column2 = mt.tag_id
|
||||
UNION
|
||||
SELECT m.id, t.column1 FROM scene_markers m
|
||||
INNER JOIN (` + valuesClause + `) t ON t.column2 = m.primary_tag_id
|
||||
)`)
|
||||
|
||||
f.addLeftJoin("marker_tags", "", "marker_tags.scene_marker_id = scene_markers.id")
|
||||
|
||||
switch tags.Modifier {
|
||||
case models.CriterionModifierEquals:
|
||||
// includes only the provided ids
|
||||
f.addWhere("marker_tags.root_tag_id IS NOT NULL")
|
||||
tagsLen := len(tags.Value)
|
||||
f.addHaving(fmt.Sprintf("count(distinct marker_tags.root_tag_id) IS %d", tagsLen))
|
||||
// decrement by one to account for primary tag id
|
||||
f.addWhere("(SELECT COUNT(*) FROM scene_markers_tags s WHERE s.scene_marker_id = scene_markers.id) = ?", tagsLen-1)
|
||||
case models.CriterionModifierNotEquals:
|
||||
f.setError(fmt.Errorf("not equals modifier is not supported for scene marker tags"))
|
||||
default:
|
||||
addHierarchicalConditionClauses(f, tags, "marker_tags", "root_tag_id")
|
||||
}
|
||||
}
|
||||
|
||||
if len(criterion.Excludes) > 0 {
|
||||
valuesClause, err := getHierarchicalValues(ctx, dbWrapper{}, tags.Excludes, tagTable, "tags_relations", "parent_id", "child_id", tags.Depth)
|
||||
if err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
clause := "scene_markers.id NOT IN (SELECT scene_markers_tags.scene_marker_id FROM scene_markers_tags WHERE scene_markers_tags.tag_id IN (SELECT column2 FROM (%s)))"
|
||||
f.addWhere(fmt.Sprintf(clause, valuesClause))
|
||||
|
||||
f.addWhere(fmt.Sprintf("scene_markers.primary_tag_id NOT IN (SELECT column2 FROM (%s))", valuesClause))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sceneMarkerSceneTagsCriterionHandler(qb *SceneMarkerStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if tags != nil {
|
||||
f.addLeftJoin("scenes_tags", "", "scene_markers.scene_id = scenes_tags.scene_id")
|
||||
|
||||
h := joinedHierarchicalMultiCriterionHandlerBuilder{
|
||||
tx: qb.tx,
|
||||
|
||||
primaryTable: "scene_markers",
|
||||
primaryKey: sceneIDColumn,
|
||||
foreignTable: tagTable,
|
||||
foreignFK: tagIDColumn,
|
||||
|
||||
relationsTable: "tags_relations",
|
||||
joinTable: "scenes_tags",
|
||||
joinAs: "marker_scenes_tags",
|
||||
primaryFK: sceneIDColumn,
|
||||
}
|
||||
|
||||
h.handler(tags).handle(ctx, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sceneMarkerPerformersCriterionHandler(qb *SceneMarkerStore, performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedMultiCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
joinTable: performersScenesTable,
|
||||
joinAs: "performers_join",
|
||||
primaryFK: sceneIDColumn,
|
||||
foreignFK: performerIDColumn,
|
||||
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
f.addLeftJoin(performersScenesTable, "performers_join", "performers_join.scene_id = scene_markers.scene_id")
|
||||
},
|
||||
}
|
||||
|
||||
handler := h.handler(performers)
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
// Make sure scenes is included, otherwise excludes filter fails
|
||||
f.addLeftJoin(sceneTable, "", "scenes.id = scene_markers.scene_id")
|
||||
handler(ctx, f)
|
||||
}
|
||||
}
|
||||
|
||||
var sceneMarkerSortOptions = sortOptions{
|
||||
"created_at",
|
||||
"id",
|
||||
@@ -514,7 +389,7 @@ func (qb *SceneMarkerStore) setSceneMarkerSort(query *queryBuilder, findFilter *
|
||||
func (qb *SceneMarkerStore) querySceneMarkers(ctx context.Context, query string, args []interface{}) ([]*models.SceneMarker, error) {
|
||||
const single = false
|
||||
var ret []*models.SceneMarker
|
||||
if err := qb.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
|
||||
if err := sceneMarkerRepository.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
|
||||
var f sceneMarkerRow
|
||||
if err := r.StructScan(&f); err != nil {
|
||||
return err
|
||||
@@ -532,7 +407,7 @@ func (qb *SceneMarkerStore) querySceneMarkers(ctx context.Context, query string,
|
||||
}
|
||||
|
||||
func (qb *SceneMarkerStore) queryMarkerStringsResultType(ctx context.Context, query string, args []interface{}) ([]*models.MarkerStringsResultType, error) {
|
||||
rows, err := qb.tx.Queryx(ctx, query, args...)
|
||||
rows, err := dbWrapper.Queryx(ctx, query, args...)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
@@ -554,24 +429,13 @@ func (qb *SceneMarkerStore) queryMarkerStringsResultType(ctx context.Context, qu
|
||||
return markerStrings, nil
|
||||
}
|
||||
|
||||
func (qb *SceneMarkerStore) tagsRepository() *joinRepository {
|
||||
return &joinRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: "scene_markers_tags",
|
||||
idColumn: "scene_marker_id",
|
||||
},
|
||||
fkColumn: tagIDColumn,
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *SceneMarkerStore) GetTagIDs(ctx context.Context, id int) ([]int, error) {
|
||||
return qb.tagsRepository().getIDs(ctx, id)
|
||||
return sceneMarkerRepository.tags.getIDs(ctx, id)
|
||||
}
|
||||
|
||||
func (qb *SceneMarkerStore) UpdateTags(ctx context.Context, id int, tagIDs []int) error {
|
||||
// Delete the existing joins and then create new ones
|
||||
return qb.tagsRepository().replace(ctx, id, tagIDs)
|
||||
return sceneMarkerRepository.tags.replace(ctx, id, tagIDs)
|
||||
}
|
||||
|
||||
func (qb *SceneMarkerStore) Count(ctx context.Context) (int, error) {
|
||||
|
||||
189
pkg/sqlite/scene_marker_filter.go
Normal file
189
pkg/sqlite/scene_marker_filter.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type sceneMarkerFilterHandler struct {
|
||||
sceneMarkerFilter *models.SceneMarkerFilterType
|
||||
}
|
||||
|
||||
func (qb *sceneMarkerFilterHandler) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *sceneMarkerFilterHandler) handle(ctx context.Context, f *filterBuilder) {
|
||||
sceneMarkerFilter := qb.sceneMarkerFilter
|
||||
if sceneMarkerFilter == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := qb.validate(); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
f.handleCriterion(ctx, qb.criterionHandler())
|
||||
}
|
||||
|
||||
func (qb *sceneMarkerFilterHandler) joinScenes(f *filterBuilder) {
|
||||
sceneMarkerRepository.scenes.innerJoin(f, "", "scene_markers.scene_id")
|
||||
}
|
||||
|
||||
func (qb *sceneMarkerFilterHandler) criterionHandler() criterionHandler {
|
||||
sceneMarkerFilter := qb.sceneMarkerFilter
|
||||
return compoundHandler{
|
||||
qb.tagIDCriterionHandler(sceneMarkerFilter.TagID),
|
||||
qb.tagsCriterionHandler(sceneMarkerFilter.Tags),
|
||||
qb.sceneTagsCriterionHandler(sceneMarkerFilter.SceneTags),
|
||||
qb.performersCriterionHandler(sceneMarkerFilter.Performers),
|
||||
×tampCriterionHandler{sceneMarkerFilter.CreatedAt, "scene_markers.created_at", nil},
|
||||
×tampCriterionHandler{sceneMarkerFilter.UpdatedAt, "scene_markers.updated_at", nil},
|
||||
&dateCriterionHandler{sceneMarkerFilter.SceneDate, "scenes.date", qb.joinScenes},
|
||||
×tampCriterionHandler{sceneMarkerFilter.SceneCreatedAt, "scenes.created_at", qb.joinScenes},
|
||||
×tampCriterionHandler{sceneMarkerFilter.SceneUpdatedAt, "scenes.updated_at", qb.joinScenes},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "scenes.id",
|
||||
relatedRepo: sceneRepository.repository,
|
||||
relatedHandler: &sceneFilterHandler{sceneMarkerFilter.SceneFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
qb.joinScenes(f)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneMarkerFilterHandler) tagIDCriterionHandler(tagID *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if tagID != nil {
|
||||
f.addLeftJoin("scene_markers_tags", "", "scene_markers_tags.scene_marker_id = scene_markers.id")
|
||||
|
||||
f.addWhere("(scene_markers.primary_tag_id = ? OR scene_markers_tags.tag_id = ?)", *tagID, *tagID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneMarkerFilterHandler) tagsCriterionHandler(criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if criterion != nil {
|
||||
tags := criterion.CombineExcludes()
|
||||
|
||||
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if tags.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
f.addLeftJoin("scene_markers_tags", "", "scene_markers.id = scene_markers_tags.scene_marker_id")
|
||||
|
||||
f.addWhere(fmt.Sprintf("%s scene_markers_tags.tag_id IS NULL", notClause))
|
||||
return
|
||||
}
|
||||
|
||||
if tags.Modifier == models.CriterionModifierEquals && tags.Depth != nil && *tags.Depth != 0 {
|
||||
f.setError(fmt.Errorf("depth is not supported for equals modifier for marker tag filtering"))
|
||||
return
|
||||
}
|
||||
|
||||
if len(tags.Value) == 0 && len(tags.Excludes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(tags.Value) > 0 {
|
||||
valuesClause, err := getHierarchicalValues(ctx, tags.Value, tagTable, "tags_relations", "parent_id", "child_id", tags.Depth)
|
||||
if err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
f.addWith(`marker_tags AS (
|
||||
SELECT mt.scene_marker_id, t.column1 AS root_tag_id FROM scene_markers_tags mt
|
||||
INNER JOIN (` + valuesClause + `) t ON t.column2 = mt.tag_id
|
||||
UNION
|
||||
SELECT m.id, t.column1 FROM scene_markers m
|
||||
INNER JOIN (` + valuesClause + `) t ON t.column2 = m.primary_tag_id
|
||||
)`)
|
||||
|
||||
f.addLeftJoin("marker_tags", "", "marker_tags.scene_marker_id = scene_markers.id")
|
||||
|
||||
switch tags.Modifier {
|
||||
case models.CriterionModifierEquals:
|
||||
// includes only the provided ids
|
||||
f.addWhere("marker_tags.root_tag_id IS NOT NULL")
|
||||
tagsLen := len(tags.Value)
|
||||
f.addHaving(fmt.Sprintf("count(distinct marker_tags.root_tag_id) IS %d", tagsLen))
|
||||
// decrement by one to account for primary tag id
|
||||
f.addWhere("(SELECT COUNT(*) FROM scene_markers_tags s WHERE s.scene_marker_id = scene_markers.id) = ?", tagsLen-1)
|
||||
case models.CriterionModifierNotEquals:
|
||||
f.setError(fmt.Errorf("not equals modifier is not supported for scene marker tags"))
|
||||
default:
|
||||
addHierarchicalConditionClauses(f, tags, "marker_tags", "root_tag_id")
|
||||
}
|
||||
}
|
||||
|
||||
if len(criterion.Excludes) > 0 {
|
||||
valuesClause, err := getHierarchicalValues(ctx, tags.Excludes, tagTable, "tags_relations", "parent_id", "child_id", tags.Depth)
|
||||
if err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
clause := "scene_markers.id NOT IN (SELECT scene_markers_tags.scene_marker_id FROM scene_markers_tags WHERE scene_markers_tags.tag_id IN (SELECT column2 FROM (%s)))"
|
||||
f.addWhere(fmt.Sprintf(clause, valuesClause))
|
||||
|
||||
f.addWhere(fmt.Sprintf("scene_markers.primary_tag_id NOT IN (SELECT column2 FROM (%s))", valuesClause))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneMarkerFilterHandler) sceneTagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if tags != nil {
|
||||
f.addLeftJoin("scenes_tags", "", "scene_markers.scene_id = scenes_tags.scene_id")
|
||||
|
||||
h := joinedHierarchicalMultiCriterionHandlerBuilder{
|
||||
primaryTable: "scene_markers",
|
||||
primaryKey: sceneIDColumn,
|
||||
foreignTable: tagTable,
|
||||
foreignFK: tagIDColumn,
|
||||
|
||||
relationsTable: "tags_relations",
|
||||
joinTable: "scenes_tags",
|
||||
joinAs: "marker_scenes_tags",
|
||||
primaryFK: sceneIDColumn,
|
||||
}
|
||||
|
||||
h.handler(tags).handle(ctx, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneMarkerFilterHandler) performersCriterionHandler(performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
h := joinedMultiCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
joinTable: performersScenesTable,
|
||||
joinAs: "performers_join",
|
||||
primaryFK: sceneIDColumn,
|
||||
foreignFK: performerIDColumn,
|
||||
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
f.addLeftJoin(performersScenesTable, "performers_join", "performers_join.scene_id = scene_markers.scene_id")
|
||||
},
|
||||
}
|
||||
|
||||
handler := h.handler(performers)
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performers == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure scenes is included, otherwise excludes filter fails
|
||||
qb.joinScenes(f)
|
||||
handler(ctx, f)
|
||||
}
|
||||
}
|
||||
@@ -2411,10 +2411,12 @@ func TestSceneQueryPathOr(t *testing.T) {
|
||||
Value: scene1Path,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
Or: &models.SceneFilterType{
|
||||
Path: &models.StringCriterionInput{
|
||||
Value: scene2Path,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
OperatorFilter: models.OperatorFilter[models.SceneFilterType]{
|
||||
Or: &models.SceneFilterType{
|
||||
Path: &models.StringCriterionInput{
|
||||
Value: scene2Path,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -2444,10 +2446,12 @@ func TestSceneQueryPathAndRating(t *testing.T) {
|
||||
Value: scenePath,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
And: &models.SceneFilterType{
|
||||
Rating100: &models.IntCriterionInput{
|
||||
Value: sceneRating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
OperatorFilter: models.OperatorFilter[models.SceneFilterType]{
|
||||
And: &models.SceneFilterType{
|
||||
Rating100: &models.IntCriterionInput{
|
||||
Value: sceneRating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -2484,8 +2488,10 @@ func TestSceneQueryPathNotRating(t *testing.T) {
|
||||
|
||||
sceneFilter := models.SceneFilterType{
|
||||
Path: &pathCriterion,
|
||||
Not: &models.SceneFilterType{
|
||||
Rating100: &ratingCriterion,
|
||||
OperatorFilter: models.OperatorFilter[models.SceneFilterType]{
|
||||
Not: &models.SceneFilterType{
|
||||
Rating100: &ratingCriterion,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2516,8 +2522,10 @@ func TestSceneIllegalQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
sceneFilter := &models.SceneFilterType{
|
||||
And: &subFilter,
|
||||
Or: &subFilter,
|
||||
OperatorFilter: models.OperatorFilter[models.SceneFilterType]{
|
||||
And: &subFilter,
|
||||
Or: &subFilter,
|
||||
},
|
||||
}
|
||||
|
||||
withTxn(func(ctx context.Context) error {
|
||||
|
||||
@@ -21,6 +21,11 @@ func distinctIDs(qb *queryBuilder, tableName string) {
|
||||
qb.from = tableName
|
||||
}
|
||||
|
||||
func selectIDs(qb *queryBuilder, tableName string) {
|
||||
qb.addColumn(getColumn(tableName, "id"))
|
||||
qb.from = tableName
|
||||
}
|
||||
|
||||
func getColumn(tableName string, columnName string) string {
|
||||
return tableName + "." + columnName
|
||||
}
|
||||
|
||||
@@ -90,8 +90,44 @@ func (r *studioRowRecord) fromPartial(o models.StudioPartial) {
|
||||
r.setBool("ignore_auto_tag", o.IgnoreAutoTag)
|
||||
}
|
||||
|
||||
type StudioStore struct {
|
||||
type studioRepositoryType struct {
|
||||
repository
|
||||
|
||||
stashIDs stashIDRepository
|
||||
|
||||
scenes repository
|
||||
images repository
|
||||
galleries repository
|
||||
}
|
||||
|
||||
var (
|
||||
studioRepository = studioRepositoryType{
|
||||
repository: repository{
|
||||
tableName: studioTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
stashIDs: stashIDRepository{
|
||||
repository{
|
||||
tableName: "studio_stash_ids",
|
||||
idColumn: studioIDColumn,
|
||||
},
|
||||
},
|
||||
scenes: repository{
|
||||
tableName: sceneTable,
|
||||
idColumn: studioIDColumn,
|
||||
},
|
||||
images: repository{
|
||||
tableName: imageTable,
|
||||
idColumn: studioIDColumn,
|
||||
},
|
||||
galleries: repository{
|
||||
tableName: galleryTable,
|
||||
idColumn: studioIDColumn,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type StudioStore struct {
|
||||
blobJoinQueryBuilder
|
||||
|
||||
tableMgr *table
|
||||
@@ -99,10 +135,6 @@ type StudioStore struct {
|
||||
|
||||
func NewStudioStore(blobStore *BlobStore) *StudioStore {
|
||||
return &StudioStore{
|
||||
repository: repository{
|
||||
tableName: studioTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
blobJoinQueryBuilder: blobJoinQueryBuilder{
|
||||
blobStore: blobStore,
|
||||
joinTable: studioTable,
|
||||
@@ -147,7 +179,7 @@ func (qb *StudioStore) Create(ctx context.Context, newObject *models.Studio) err
|
||||
}
|
||||
}
|
||||
|
||||
updated, _ := qb.find(ctx, id)
|
||||
updated, err := qb.find(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding after create: %w", err)
|
||||
}
|
||||
@@ -220,7 +252,7 @@ func (qb *StudioStore) Destroy(ctx context.Context, id int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return qb.destroyExisting(ctx, []int{id})
|
||||
return studioRepository.destroyExisting(ctx, []int{id})
|
||||
}
|
||||
|
||||
// returns nil, nil if not found
|
||||
@@ -452,83 +484,6 @@ func (qb *StudioStore) QueryForAutoTag(ctx context.Context, words []string) ([]*
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (qb *StudioStore) validateFilter(filter *models.StudioFilterType) error {
|
||||
const and = "AND"
|
||||
const or = "OR"
|
||||
const not = "NOT"
|
||||
|
||||
if filter.And != nil {
|
||||
if filter.Or != nil {
|
||||
return illegalFilterCombination(and, or)
|
||||
}
|
||||
if filter.Not != nil {
|
||||
return illegalFilterCombination(and, not)
|
||||
}
|
||||
|
||||
return qb.validateFilter(filter.And)
|
||||
}
|
||||
|
||||
if filter.Or != nil {
|
||||
if filter.Not != nil {
|
||||
return illegalFilterCombination(or, not)
|
||||
}
|
||||
|
||||
return qb.validateFilter(filter.Or)
|
||||
}
|
||||
|
||||
if filter.Not != nil {
|
||||
return qb.validateFilter(filter.Not)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *StudioStore) makeFilter(ctx context.Context, studioFilter *models.StudioFilterType) *filterBuilder {
|
||||
query := &filterBuilder{}
|
||||
|
||||
if studioFilter.And != nil {
|
||||
query.and(qb.makeFilter(ctx, studioFilter.And))
|
||||
}
|
||||
if studioFilter.Or != nil {
|
||||
query.or(qb.makeFilter(ctx, studioFilter.Or))
|
||||
}
|
||||
if studioFilter.Not != nil {
|
||||
query.not(qb.makeFilter(ctx, studioFilter.Not))
|
||||
}
|
||||
|
||||
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Name, studioTable+".name"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Details, studioTable+".details"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.URL, studioTable+".url"))
|
||||
query.handleCriterion(ctx, intCriterionHandler(studioFilter.Rating100, studioTable+".rating", nil))
|
||||
query.handleCriterion(ctx, boolCriterionHandler(studioFilter.Favorite, studioTable+".favorite", nil))
|
||||
query.handleCriterion(ctx, boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag", nil))
|
||||
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if studioFilter.StashID != nil {
|
||||
qb.stashIDRepository().join(f, "studio_stash_ids", "studios.id")
|
||||
stringCriterionHandler(studioFilter.StashID, "studio_stash_ids.stash_id")(ctx, f)
|
||||
}
|
||||
}))
|
||||
query.handleCriterion(ctx, &stashIDCriterionHandler{
|
||||
c: studioFilter.StashIDEndpoint,
|
||||
stashIDRepository: qb.stashIDRepository(),
|
||||
stashIDTableAs: "studio_stash_ids",
|
||||
parentIDCol: "studios.id",
|
||||
})
|
||||
|
||||
query.handleCriterion(ctx, studioIsMissingCriterionHandler(qb, studioFilter.IsMissing))
|
||||
query.handleCriterion(ctx, studioSceneCountCriterionHandler(qb, studioFilter.SceneCount))
|
||||
query.handleCriterion(ctx, studioImageCountCriterionHandler(qb, studioFilter.ImageCount))
|
||||
query.handleCriterion(ctx, studioGalleryCountCriterionHandler(qb, studioFilter.GalleryCount))
|
||||
query.handleCriterion(ctx, studioParentCriterionHandler(qb, studioFilter.Parents))
|
||||
query.handleCriterion(ctx, studioAliasCriterionHandler(qb, studioFilter.Aliases))
|
||||
query.handleCriterion(ctx, studioChildCountCriterionHandler(qb, studioFilter.ChildCount))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(studioFilter.CreatedAt, studioTable+".created_at"))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(studioFilter.UpdatedAt, studioTable+".updated_at"))
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (qb *StudioStore) makeQuery(ctx context.Context, studioFilter *models.StudioFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
|
||||
if studioFilter == nil {
|
||||
studioFilter = &models.StudioFilterType{}
|
||||
@@ -537,7 +492,7 @@ func (qb *StudioStore) makeQuery(ctx context.Context, studioFilter *models.Studi
|
||||
findFilter = &models.FindFilterType{}
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
query := studioRepository.newQuery()
|
||||
distinctIDs(&query, studioTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
@@ -546,10 +501,9 @@ func (qb *StudioStore) makeQuery(ctx context.Context, studioFilter *models.Studi
|
||||
query.parseQueryString(searchColumns, *q)
|
||||
}
|
||||
|
||||
if err := qb.validateFilter(studioFilter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter := qb.makeFilter(ctx, studioFilter)
|
||||
filter := filterBuilderFromHandler(ctx, &studioFilterHandler{
|
||||
studioFilter: studioFilter,
|
||||
})
|
||||
|
||||
if err := query.addFilter(filter); err != nil {
|
||||
return nil, err
|
||||
@@ -584,93 +538,6 @@ func (qb *StudioStore) Query(ctx context.Context, studioFilter *models.StudioFil
|
||||
return studios, countResult, nil
|
||||
}
|
||||
|
||||
func studioIsMissingCriterionHandler(qb *StudioStore, isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "image":
|
||||
f.addWhere("studios.image_blob IS NULL")
|
||||
case "stash_id":
|
||||
qb.stashIDRepository().join(f, "studio_stash_ids", "studios.id")
|
||||
f.addWhere("studio_stash_ids.studio_id IS NULL")
|
||||
default:
|
||||
f.addWhere("(studios." + *isMissing + " IS NULL OR TRIM(studios." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func studioSceneCountCriterionHandler(qb *StudioStore, sceneCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if sceneCount != nil {
|
||||
f.addLeftJoin("scenes", "", "scenes.studio_id = studios.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct scenes.id)", *sceneCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func studioImageCountCriterionHandler(qb *StudioStore, imageCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if imageCount != nil {
|
||||
f.addLeftJoin("images", "", "images.studio_id = studios.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct images.id)", *imageCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func studioGalleryCountCriterionHandler(qb *StudioStore, galleryCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if galleryCount != nil {
|
||||
f.addLeftJoin("galleries", "", "galleries.studio_id = studios.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct galleries.id)", *galleryCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func studioParentCriterionHandler(qb *StudioStore, parents *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
f.addLeftJoin("studios", "parent_studio", "parent_studio.id = studios.parent_id")
|
||||
}
|
||||
h := multiCriterionHandlerBuilder{
|
||||
primaryTable: studioTable,
|
||||
foreignTable: "parent_studio",
|
||||
joinTable: "",
|
||||
primaryFK: studioIDColumn,
|
||||
foreignFK: "parent_id",
|
||||
addJoinsFunc: addJoinsFunc,
|
||||
}
|
||||
return h.handler(parents)
|
||||
}
|
||||
|
||||
func studioAliasCriterionHandler(qb *StudioStore, alias *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: studioAliasesTable,
|
||||
stringColumn: studioAliasColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
studiosAliasesTableMgr.join(f, "", "studios.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(alias)
|
||||
}
|
||||
|
||||
func studioChildCountCriterionHandler(qb *StudioStore, childCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if childCount != nil {
|
||||
f.addLeftJoin("studios", "children_count", "children_count.parent_id = studios.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct children_count.id)", *childCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var studioSortOptions = sortOptions{
|
||||
"child_count",
|
||||
"created_at",
|
||||
@@ -735,16 +602,6 @@ func (qb *StudioStore) destroyImage(ctx context.Context, studioID int) error {
|
||||
return qb.blobJoinQueryBuilder.DestroyImage(ctx, studioID, studioImageBlobColumn)
|
||||
}
|
||||
|
||||
func (qb *StudioStore) stashIDRepository() *stashIDRepository {
|
||||
return &stashIDRepository{
|
||||
repository{
|
||||
tx: qb.tx,
|
||||
tableName: "studio_stash_ids",
|
||||
idColumn: studioIDColumn,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *StudioStore) GetStashIDs(ctx context.Context, studioID int) ([]models.StashID, error) {
|
||||
return studiosStashIDsTableMgr.get(ctx, studioID)
|
||||
}
|
||||
|
||||
200
pkg/sqlite/studio_filter.go
Normal file
200
pkg/sqlite/studio_filter.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type studioFilterHandler struct {
|
||||
studioFilter *models.StudioFilterType
|
||||
}
|
||||
|
||||
func (qb *studioFilterHandler) validate() error {
|
||||
studioFilter := qb.studioFilter
|
||||
if studioFilter == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := validateFilterCombination(studioFilter.OperatorFilter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if subFilter := studioFilter.SubFilter(); subFilter != nil {
|
||||
sqb := &studioFilterHandler{studioFilter: subFilter}
|
||||
if err := sqb.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *studioFilterHandler) handle(ctx context.Context, f *filterBuilder) {
|
||||
studioFilter := qb.studioFilter
|
||||
if studioFilter == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := qb.validate(); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
sf := studioFilter.SubFilter()
|
||||
if sf != nil {
|
||||
sub := &studioFilterHandler{sf}
|
||||
handleSubFilter(ctx, sub, f, studioFilter.OperatorFilter)
|
||||
}
|
||||
|
||||
f.handleCriterion(ctx, qb.criterionHandler())
|
||||
}
|
||||
|
||||
func (qb *studioFilterHandler) criterionHandler() criterionHandler {
|
||||
studioFilter := qb.studioFilter
|
||||
return compoundHandler{
|
||||
stringCriterionHandler(studioFilter.Name, studioTable+".name"),
|
||||
stringCriterionHandler(studioFilter.Details, studioTable+".details"),
|
||||
stringCriterionHandler(studioFilter.URL, studioTable+".url"),
|
||||
intCriterionHandler(studioFilter.Rating100, studioTable+".rating", nil),
|
||||
boolCriterionHandler(studioFilter.Favorite, studioTable+".favorite", nil),
|
||||
boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag", nil),
|
||||
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if studioFilter.StashID != nil {
|
||||
studioRepository.stashIDs.join(f, "studio_stash_ids", "studios.id")
|
||||
stringCriterionHandler(studioFilter.StashID, "studio_stash_ids.stash_id")(ctx, f)
|
||||
}
|
||||
}),
|
||||
&stashIDCriterionHandler{
|
||||
c: studioFilter.StashIDEndpoint,
|
||||
stashIDRepository: &studioRepository.stashIDs,
|
||||
stashIDTableAs: "studio_stash_ids",
|
||||
parentIDCol: "studios.id",
|
||||
},
|
||||
|
||||
qb.isMissingCriterionHandler(studioFilter.IsMissing),
|
||||
qb.sceneCountCriterionHandler(studioFilter.SceneCount),
|
||||
qb.imageCountCriterionHandler(studioFilter.ImageCount),
|
||||
qb.galleryCountCriterionHandler(studioFilter.GalleryCount),
|
||||
qb.parentCriterionHandler(studioFilter.Parents),
|
||||
qb.aliasCriterionHandler(studioFilter.Aliases),
|
||||
qb.childCountCriterionHandler(studioFilter.ChildCount),
|
||||
×tampCriterionHandler{studioFilter.CreatedAt, studioTable + ".created_at", nil},
|
||||
×tampCriterionHandler{studioFilter.UpdatedAt, studioTable + ".updated_at", nil},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "scenes.id",
|
||||
relatedRepo: sceneRepository.repository,
|
||||
relatedHandler: &sceneFilterHandler{studioFilter.ScenesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
sceneRepository.innerJoin(f, "", "studios.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "images.id",
|
||||
relatedRepo: imageRepository.repository,
|
||||
relatedHandler: &imageFilterHandler{studioFilter.ImagesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
studioRepository.images.innerJoin(f, "", "studios.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "galleries.id",
|
||||
relatedRepo: galleryRepository.repository,
|
||||
relatedHandler: &galleryFilterHandler{studioFilter.GalleriesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
studioRepository.galleries.innerJoin(f, "", "studios.id")
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *studioFilterHandler) isMissingCriterionHandler(isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "image":
|
||||
f.addWhere("studios.image_blob IS NULL")
|
||||
case "stash_id":
|
||||
studioRepository.stashIDs.join(f, "studio_stash_ids", "studios.id")
|
||||
f.addWhere("studio_stash_ids.studio_id IS NULL")
|
||||
default:
|
||||
f.addWhere("(studios." + *isMissing + " IS NULL OR TRIM(studios." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *studioFilterHandler) sceneCountCriterionHandler(sceneCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if sceneCount != nil {
|
||||
f.addLeftJoin("scenes", "", "scenes.studio_id = studios.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct scenes.id)", *sceneCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *studioFilterHandler) imageCountCriterionHandler(imageCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if imageCount != nil {
|
||||
f.addLeftJoin("images", "", "images.studio_id = studios.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct images.id)", *imageCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *studioFilterHandler) galleryCountCriterionHandler(galleryCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if galleryCount != nil {
|
||||
f.addLeftJoin("galleries", "", "galleries.studio_id = studios.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct galleries.id)", *galleryCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *studioFilterHandler) parentCriterionHandler(parents *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
f.addLeftJoin("studios", "parent_studio", "parent_studio.id = studios.parent_id")
|
||||
}
|
||||
h := multiCriterionHandlerBuilder{
|
||||
primaryTable: studioTable,
|
||||
foreignTable: "parent_studio",
|
||||
joinTable: "",
|
||||
primaryFK: studioIDColumn,
|
||||
foreignFK: "parent_id",
|
||||
addJoinsFunc: addJoinsFunc,
|
||||
}
|
||||
return h.handler(parents)
|
||||
}
|
||||
|
||||
func (qb *studioFilterHandler) aliasCriterionHandler(alias *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: studioAliasesTable,
|
||||
stringColumn: studioAliasColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
studiosAliasesTableMgr.join(f, "", "studios.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(alias)
|
||||
}
|
||||
|
||||
func (qb *studioFilterHandler) childCountCriterionHandler(childCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if childCount != nil {
|
||||
f.addLeftJoin("studios", "children_count", "children_count.parent_id = studios.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct children_count.id)", *childCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,10 +59,12 @@ func TestStudioQueryNameOr(t *testing.T) {
|
||||
Value: studio1Name,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
Or: &models.StudioFilterType{
|
||||
Name: &models.StringCriterionInput{
|
||||
Value: studio2Name,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
OperatorFilter: models.OperatorFilter[models.StudioFilterType]{
|
||||
Or: &models.StudioFilterType{
|
||||
Name: &models.StringCriterionInput{
|
||||
Value: studio2Name,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -90,10 +92,12 @@ func TestStudioQueryNameAndUrl(t *testing.T) {
|
||||
Value: studioName,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
And: &models.StudioFilterType{
|
||||
URL: &models.StringCriterionInput{
|
||||
Value: studioUrl,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
OperatorFilter: models.OperatorFilter[models.StudioFilterType]{
|
||||
And: &models.StudioFilterType{
|
||||
URL: &models.StringCriterionInput{
|
||||
Value: studioUrl,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -128,8 +132,10 @@ func TestStudioQueryNameNotUrl(t *testing.T) {
|
||||
|
||||
studioFilter := models.StudioFilterType{
|
||||
Name: &nameCriterion,
|
||||
Not: &models.StudioFilterType{
|
||||
URL: &urlCriterion,
|
||||
OperatorFilter: models.OperatorFilter[models.StudioFilterType]{
|
||||
Not: &models.StudioFilterType{
|
||||
URL: &urlCriterion,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -160,8 +166,10 @@ func TestStudioIllegalQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
studioFilter := &models.StudioFilterType{
|
||||
And: &subFilter,
|
||||
Or: &subFilter,
|
||||
OperatorFilter: models.OperatorFilter[models.StudioFilterType]{
|
||||
And: &subFilter,
|
||||
Or: &subFilter,
|
||||
},
|
||||
}
|
||||
|
||||
withTxn(func(ctx context.Context) error {
|
||||
|
||||
@@ -193,8 +193,7 @@ func (t *joinTable) insertJoins(ctx context.Context, id int, foreignIDs []int) e
|
||||
// ignore duplicates
|
||||
q := fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?) ON CONFLICT (%[2]s, %s) DO NOTHING", t.table.table.GetTable(), t.idColumn.GetCol(), t.fkColumn.GetCol())
|
||||
|
||||
tx := dbWrapper{}
|
||||
stmt, err := tx.Prepare(ctx, q)
|
||||
stmt, err := dbWrapper.Prepare(ctx, q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -204,7 +203,7 @@ func (t *joinTable) insertJoins(ctx context.Context, id int, foreignIDs []int) e
|
||||
foreignIDs = sliceutil.AppendUniques(nil, foreignIDs)
|
||||
|
||||
for _, fk := range foreignIDs {
|
||||
if _, err := tx.ExecStmt(ctx, stmt, id, fk); err != nil {
|
||||
if _, err := dbWrapper.ExecStmt(ctx, stmt, id, fk); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1077,8 +1076,7 @@ func queryFunc(ctx context.Context, query *goqu.SelectDataset, single bool, f fu
|
||||
return err
|
||||
}
|
||||
|
||||
wrapper := dbWrapper{}
|
||||
rows, err := wrapper.QueryxContext(ctx, q, args...)
|
||||
rows, err := dbWrapper.QueryxContext(ctx, q, args...)
|
||||
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("querying `%s` [%v]: %w", q, args, err)
|
||||
@@ -1107,8 +1105,7 @@ func querySimple(ctx context.Context, query *goqu.SelectDataset, out interface{}
|
||||
return err
|
||||
}
|
||||
|
||||
wrapper := dbWrapper{}
|
||||
rows, err := wrapper.QueryxContext(ctx, q, args...)
|
||||
rows, err := dbWrapper.QueryxContext(ctx, q, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("querying `%s` [%v]: %w", q, args, err)
|
||||
}
|
||||
|
||||
@@ -90,8 +90,57 @@ func (r *tagRowRecord) fromPartial(o models.TagPartial) {
|
||||
r.setTimestamp("updated_at", o.UpdatedAt)
|
||||
}
|
||||
|
||||
type TagStore struct {
|
||||
type tagRepositoryType struct {
|
||||
repository
|
||||
|
||||
aliases stringRepository
|
||||
|
||||
scenes joinRepository
|
||||
images joinRepository
|
||||
galleries joinRepository
|
||||
}
|
||||
|
||||
var (
|
||||
tagRepository = tagRepositoryType{
|
||||
repository: repository{
|
||||
tableName: tagTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
aliases: stringRepository{
|
||||
repository: repository{
|
||||
tableName: tagAliasesTable,
|
||||
idColumn: tagIDColumn,
|
||||
},
|
||||
stringColumn: tagAliasColumn,
|
||||
},
|
||||
scenes: joinRepository{
|
||||
repository: repository{
|
||||
tableName: scenesTagsTable,
|
||||
idColumn: tagIDColumn,
|
||||
},
|
||||
fkColumn: sceneIDColumn,
|
||||
foreignTable: sceneTable,
|
||||
},
|
||||
images: joinRepository{
|
||||
repository: repository{
|
||||
tableName: imagesTagsTable,
|
||||
idColumn: tagIDColumn,
|
||||
},
|
||||
fkColumn: imageIDColumn,
|
||||
foreignTable: imageTable,
|
||||
},
|
||||
galleries: joinRepository{
|
||||
repository: repository{
|
||||
tableName: galleriesTagsTable,
|
||||
idColumn: tagIDColumn,
|
||||
},
|
||||
fkColumn: galleryIDColumn,
|
||||
foreignTable: galleryTable,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type TagStore struct {
|
||||
blobJoinQueryBuilder
|
||||
|
||||
tableMgr *table
|
||||
@@ -99,10 +148,6 @@ type TagStore struct {
|
||||
|
||||
func NewTagStore(blobStore *BlobStore) *TagStore {
|
||||
return &TagStore{
|
||||
repository: repository{
|
||||
tableName: tagTable,
|
||||
idColumn: idColumn,
|
||||
},
|
||||
blobJoinQueryBuilder: blobJoinQueryBuilder{
|
||||
blobStore: blobStore,
|
||||
joinTable: tagTable,
|
||||
@@ -176,7 +221,7 @@ func (qb *TagStore) Destroy(ctx context.Context, id int) error {
|
||||
// cannot unset primary_tag_id in scene_markers because it is not nullable
|
||||
countQuery := "SELECT COUNT(*) as count FROM scene_markers where primary_tag_id = ?"
|
||||
args := []interface{}{id}
|
||||
primaryMarkers, err := qb.runCountQuery(ctx, countQuery, args)
|
||||
primaryMarkers, err := tagRepository.runCountQuery(ctx, countQuery, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -185,7 +230,7 @@ func (qb *TagStore) Destroy(ctx context.Context, id int) error {
|
||||
return errors.New("cannot delete tag used as a primary tag in scene markers")
|
||||
}
|
||||
|
||||
return qb.destroyExisting(ctx, []int{id})
|
||||
return tagRepository.destroyExisting(ctx, []int{id})
|
||||
}
|
||||
|
||||
// returns nil, nil if not found
|
||||
@@ -455,73 +500,6 @@ func (qb *TagStore) QueryForAutoTag(ctx context.Context, words []string) ([]*mod
|
||||
return qb.queryTags(ctx, query+" WHERE "+where, args)
|
||||
}
|
||||
|
||||
func (qb *TagStore) validateFilter(tagFilter *models.TagFilterType) error {
|
||||
const and = "AND"
|
||||
const or = "OR"
|
||||
const not = "NOT"
|
||||
|
||||
if tagFilter.And != nil {
|
||||
if tagFilter.Or != nil {
|
||||
return illegalFilterCombination(and, or)
|
||||
}
|
||||
if tagFilter.Not != nil {
|
||||
return illegalFilterCombination(and, not)
|
||||
}
|
||||
|
||||
return qb.validateFilter(tagFilter.And)
|
||||
}
|
||||
|
||||
if tagFilter.Or != nil {
|
||||
if tagFilter.Not != nil {
|
||||
return illegalFilterCombination(or, not)
|
||||
}
|
||||
|
||||
return qb.validateFilter(tagFilter.Or)
|
||||
}
|
||||
|
||||
if tagFilter.Not != nil {
|
||||
return qb.validateFilter(tagFilter.Not)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *TagStore) makeFilter(ctx context.Context, tagFilter *models.TagFilterType) *filterBuilder {
|
||||
query := &filterBuilder{}
|
||||
|
||||
if tagFilter.And != nil {
|
||||
query.and(qb.makeFilter(ctx, tagFilter.And))
|
||||
}
|
||||
if tagFilter.Or != nil {
|
||||
query.or(qb.makeFilter(ctx, tagFilter.Or))
|
||||
}
|
||||
if tagFilter.Not != nil {
|
||||
query.not(qb.makeFilter(ctx, tagFilter.Not))
|
||||
}
|
||||
|
||||
query.handleCriterion(ctx, stringCriterionHandler(tagFilter.Name, tagTable+".name"))
|
||||
query.handleCriterion(ctx, tagAliasCriterionHandler(qb, tagFilter.Aliases))
|
||||
|
||||
query.handleCriterion(ctx, boolCriterionHandler(tagFilter.Favorite, tagTable+".favorite", nil))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(tagFilter.Description, tagTable+".description"))
|
||||
query.handleCriterion(ctx, boolCriterionHandler(tagFilter.IgnoreAutoTag, tagTable+".ignore_auto_tag", nil))
|
||||
|
||||
query.handleCriterion(ctx, tagIsMissingCriterionHandler(qb, tagFilter.IsMissing))
|
||||
query.handleCriterion(ctx, tagSceneCountCriterionHandler(qb, tagFilter.SceneCount))
|
||||
query.handleCriterion(ctx, tagImageCountCriterionHandler(qb, tagFilter.ImageCount))
|
||||
query.handleCriterion(ctx, tagGalleryCountCriterionHandler(qb, tagFilter.GalleryCount))
|
||||
query.handleCriterion(ctx, tagPerformerCountCriterionHandler(qb, tagFilter.PerformerCount))
|
||||
query.handleCriterion(ctx, tagMarkerCountCriterionHandler(qb, tagFilter.MarkerCount))
|
||||
query.handleCriterion(ctx, tagParentsCriterionHandler(qb, tagFilter.Parents))
|
||||
query.handleCriterion(ctx, tagChildrenCriterionHandler(qb, tagFilter.Children))
|
||||
query.handleCriterion(ctx, tagParentCountCriterionHandler(qb, tagFilter.ParentCount))
|
||||
query.handleCriterion(ctx, tagChildCountCriterionHandler(qb, tagFilter.ChildCount))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(tagFilter.CreatedAt, "tags.created_at"))
|
||||
query.handleCriterion(ctx, timestampCriterionHandler(tagFilter.UpdatedAt, "tags.updated_at"))
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (qb *TagStore) Query(ctx context.Context, tagFilter *models.TagFilterType, findFilter *models.FindFilterType) ([]*models.Tag, int, error) {
|
||||
if tagFilter == nil {
|
||||
tagFilter = &models.TagFilterType{}
|
||||
@@ -530,7 +508,7 @@ func (qb *TagStore) Query(ctx context.Context, tagFilter *models.TagFilterType,
|
||||
findFilter = &models.FindFilterType{}
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
query := tagRepository.newQuery()
|
||||
distinctIDs(&query, tagTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
@@ -539,10 +517,9 @@ func (qb *TagStore) Query(ctx context.Context, tagFilter *models.TagFilterType,
|
||||
query.parseQueryString(searchColumns, *q)
|
||||
}
|
||||
|
||||
if err := qb.validateFilter(tagFilter); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
filter := qb.makeFilter(ctx, tagFilter)
|
||||
filter := filterBuilderFromHandler(ctx, &tagFilterHandler{
|
||||
tagFilter: tagFilter,
|
||||
})
|
||||
|
||||
if err := query.addFilter(filter); err != nil {
|
||||
return nil, 0, err
|
||||
@@ -567,297 +544,6 @@ func (qb *TagStore) Query(ctx context.Context, tagFilter *models.TagFilterType,
|
||||
return tags, countResult, nil
|
||||
}
|
||||
|
||||
func tagAliasCriterionHandler(qb *TagStore, alias *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: tagAliasesTable,
|
||||
stringColumn: tagAliasColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
qb.aliasRepository().join(f, "", "tags.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(alias)
|
||||
}
|
||||
|
||||
func tagIsMissingCriterionHandler(qb *TagStore, isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "image":
|
||||
f.addWhere("tags.image_blob IS NULL")
|
||||
default:
|
||||
f.addWhere("(tags." + *isMissing + " IS NULL OR TRIM(tags." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tagSceneCountCriterionHandler(qb *TagStore, sceneCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if sceneCount != nil {
|
||||
f.addLeftJoin("scenes_tags", "", "scenes_tags.tag_id = tags.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct scenes_tags.scene_id)", *sceneCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tagImageCountCriterionHandler(qb *TagStore, imageCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if imageCount != nil {
|
||||
f.addLeftJoin("images_tags", "", "images_tags.tag_id = tags.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct images_tags.image_id)", *imageCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tagGalleryCountCriterionHandler(qb *TagStore, galleryCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if galleryCount != nil {
|
||||
f.addLeftJoin("galleries_tags", "", "galleries_tags.tag_id = tags.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct galleries_tags.gallery_id)", *galleryCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tagPerformerCountCriterionHandler(qb *TagStore, performerCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performerCount != nil {
|
||||
f.addLeftJoin("performers_tags", "", "performers_tags.tag_id = tags.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct performers_tags.performer_id)", *performerCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tagMarkerCountCriterionHandler(qb *TagStore, markerCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if markerCount != nil {
|
||||
f.addLeftJoin("scene_markers_tags", "", "scene_markers_tags.tag_id = tags.id")
|
||||
f.addLeftJoin("scene_markers", "", "scene_markers_tags.scene_marker_id = scene_markers.id OR scene_markers.primary_tag_id = tags.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct scene_markers.id)", *markerCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tagParentsCriterionHandler(qb *TagStore, criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if criterion != nil {
|
||||
tags := criterion.CombineExcludes()
|
||||
|
||||
// validate the modifier
|
||||
switch tags.Modifier {
|
||||
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
|
||||
// valid
|
||||
default:
|
||||
f.setError(fmt.Errorf("invalid modifier %s for tag parent/children", criterion.Modifier))
|
||||
}
|
||||
|
||||
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if tags.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
f.addLeftJoin("tags_relations", "parent_relations", "tags.id = parent_relations.child_id")
|
||||
|
||||
f.addWhere(fmt.Sprintf("parent_relations.parent_id IS %s NULL", notClause))
|
||||
return
|
||||
}
|
||||
|
||||
if len(tags.Value) == 0 && len(tags.Excludes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(tags.Value) > 0 {
|
||||
var args []interface{}
|
||||
for _, val := range tags.Value {
|
||||
args = append(args, val)
|
||||
}
|
||||
|
||||
depthVal := 0
|
||||
if tags.Depth != nil {
|
||||
depthVal = *tags.Depth
|
||||
}
|
||||
|
||||
var depthCondition string
|
||||
if depthVal != -1 {
|
||||
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
|
||||
}
|
||||
|
||||
query := `parents AS (
|
||||
SELECT parent_id AS root_id, child_id AS item_id, 0 AS depth FROM tags_relations WHERE parent_id IN` + getInBinding(len(tags.Value)) + `
|
||||
UNION
|
||||
SELECT root_id, child_id, depth + 1 FROM tags_relations INNER JOIN parents ON item_id = parent_id ` + depthCondition + `
|
||||
)`
|
||||
|
||||
f.addRecursiveWith(query, args...)
|
||||
|
||||
f.addLeftJoin("parents", "", "parents.item_id = tags.id")
|
||||
|
||||
addHierarchicalConditionClauses(f, tags, "parents", "root_id")
|
||||
}
|
||||
|
||||
if len(tags.Excludes) > 0 {
|
||||
var args []interface{}
|
||||
for _, val := range tags.Excludes {
|
||||
args = append(args, val)
|
||||
}
|
||||
|
||||
depthVal := 0
|
||||
if tags.Depth != nil {
|
||||
depthVal = *tags.Depth
|
||||
}
|
||||
|
||||
var depthCondition string
|
||||
if depthVal != -1 {
|
||||
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
|
||||
}
|
||||
|
||||
query := `parents2 AS (
|
||||
SELECT parent_id AS root_id, child_id AS item_id, 0 AS depth FROM tags_relations WHERE parent_id IN` + getInBinding(len(tags.Excludes)) + `
|
||||
UNION
|
||||
SELECT root_id, child_id, depth + 1 FROM tags_relations INNER JOIN parents2 ON item_id = parent_id ` + depthCondition + `
|
||||
)`
|
||||
|
||||
f.addRecursiveWith(query, args...)
|
||||
|
||||
f.addLeftJoin("parents2", "", "parents2.item_id = tags.id")
|
||||
|
||||
addHierarchicalConditionClauses(f, models.HierarchicalMultiCriterionInput{
|
||||
Value: tags.Excludes,
|
||||
Depth: tags.Depth,
|
||||
Modifier: models.CriterionModifierExcludes,
|
||||
}, "parents2", "root_id")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tagChildrenCriterionHandler(qb *TagStore, criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if criterion != nil {
|
||||
tags := criterion.CombineExcludes()
|
||||
|
||||
// validate the modifier
|
||||
switch tags.Modifier {
|
||||
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
|
||||
// valid
|
||||
default:
|
||||
f.setError(fmt.Errorf("invalid modifier %s for tag parent/children", criterion.Modifier))
|
||||
}
|
||||
|
||||
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if tags.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
f.addLeftJoin("tags_relations", "child_relations", "tags.id = child_relations.parent_id")
|
||||
|
||||
f.addWhere(fmt.Sprintf("child_relations.child_id IS %s NULL", notClause))
|
||||
return
|
||||
}
|
||||
|
||||
if len(tags.Value) == 0 && len(tags.Excludes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(tags.Value) > 0 {
|
||||
var args []interface{}
|
||||
for _, val := range tags.Value {
|
||||
args = append(args, val)
|
||||
}
|
||||
|
||||
depthVal := 0
|
||||
if tags.Depth != nil {
|
||||
depthVal = *tags.Depth
|
||||
}
|
||||
|
||||
var depthCondition string
|
||||
if depthVal != -1 {
|
||||
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
|
||||
}
|
||||
|
||||
query := `children AS (
|
||||
SELECT child_id AS root_id, parent_id AS item_id, 0 AS depth FROM tags_relations WHERE child_id IN` + getInBinding(len(tags.Value)) + `
|
||||
UNION
|
||||
SELECT root_id, parent_id, depth + 1 FROM tags_relations INNER JOIN children ON item_id = child_id ` + depthCondition + `
|
||||
)`
|
||||
|
||||
f.addRecursiveWith(query, args...)
|
||||
|
||||
f.addLeftJoin("children", "", "children.item_id = tags.id")
|
||||
|
||||
addHierarchicalConditionClauses(f, tags, "children", "root_id")
|
||||
}
|
||||
|
||||
if len(tags.Excludes) > 0 {
|
||||
var args []interface{}
|
||||
for _, val := range tags.Excludes {
|
||||
args = append(args, val)
|
||||
}
|
||||
|
||||
depthVal := 0
|
||||
if tags.Depth != nil {
|
||||
depthVal = *tags.Depth
|
||||
}
|
||||
|
||||
var depthCondition string
|
||||
if depthVal != -1 {
|
||||
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
|
||||
}
|
||||
|
||||
query := `children2 AS (
|
||||
SELECT child_id AS root_id, parent_id AS item_id, 0 AS depth FROM tags_relations WHERE child_id IN` + getInBinding(len(tags.Excludes)) + `
|
||||
UNION
|
||||
SELECT root_id, parent_id, depth + 1 FROM tags_relations INNER JOIN children2 ON item_id = child_id ` + depthCondition + `
|
||||
)`
|
||||
|
||||
f.addRecursiveWith(query, args...)
|
||||
|
||||
f.addLeftJoin("children2", "", "children2.item_id = tags.id")
|
||||
|
||||
addHierarchicalConditionClauses(f, models.HierarchicalMultiCriterionInput{
|
||||
Value: tags.Excludes,
|
||||
Depth: tags.Depth,
|
||||
Modifier: models.CriterionModifierExcludes,
|
||||
}, "children2", "root_id")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tagParentCountCriterionHandler(qb *TagStore, parentCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if parentCount != nil {
|
||||
f.addLeftJoin("tags_relations", "parents_count", "parents_count.child_id = tags.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct parents_count.parent_id)", *parentCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tagChildCountCriterionHandler(qb *TagStore, childCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if childCount != nil {
|
||||
f.addLeftJoin("tags_relations", "children_count", "children_count.parent_id = tags.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct children_count.child_id)", *childCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tagSortOptions = sortOptions{
|
||||
"created_at",
|
||||
"galleries_count",
|
||||
@@ -915,7 +601,7 @@ func (qb *TagStore) getTagSort(query *queryBuilder, findFilter *models.FindFilte
|
||||
func (qb *TagStore) queryTags(ctx context.Context, query string, args []interface{}) ([]*models.Tag, error) {
|
||||
const single = false
|
||||
var ret []*models.Tag
|
||||
if err := qb.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
|
||||
if err := tagRepository.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
|
||||
var f tagRow
|
||||
if err := r.StructScan(&f); err != nil {
|
||||
return err
|
||||
@@ -935,7 +621,7 @@ func (qb *TagStore) queryTags(ctx context.Context, query string, args []interfac
|
||||
func (qb *TagStore) queryTagPaths(ctx context.Context, query string, args []interface{}) ([]*models.TagPath, error) {
|
||||
const single = false
|
||||
var ret []*models.TagPath
|
||||
if err := qb.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
|
||||
if err := tagRepository.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
|
||||
var f tagPathRow
|
||||
if err := r.StructScan(&f); err != nil {
|
||||
return err
|
||||
@@ -968,23 +654,12 @@ func (qb *TagStore) destroyImage(ctx context.Context, tagID int) error {
|
||||
return qb.blobJoinQueryBuilder.DestroyImage(ctx, tagID, tagImageBlobColumn)
|
||||
}
|
||||
|
||||
func (qb *TagStore) aliasRepository() *stringRepository {
|
||||
return &stringRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: tagAliasesTable,
|
||||
idColumn: tagIDColumn,
|
||||
},
|
||||
stringColumn: tagAliasColumn,
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *TagStore) GetAliases(ctx context.Context, tagID int) ([]string, error) {
|
||||
return qb.aliasRepository().get(ctx, tagID)
|
||||
return tagRepository.aliases.get(ctx, tagID)
|
||||
}
|
||||
|
||||
func (qb *TagStore) UpdateAliases(ctx context.Context, tagID int, aliases []string) error {
|
||||
return qb.aliasRepository().replace(ctx, tagID, aliases)
|
||||
return tagRepository.aliases.replace(ctx, tagID, aliases)
|
||||
}
|
||||
|
||||
func (qb *TagStore) Merge(ctx context.Context, source []int, destination int) error {
|
||||
@@ -1015,7 +690,7 @@ func (qb *TagStore) Merge(ctx context.Context, source []int, destination int) er
|
||||
|
||||
args = append(args, destination)
|
||||
for table, idColumn := range tagTables {
|
||||
_, err := qb.tx.Exec(ctx, `UPDATE OR IGNORE `+table+`
|
||||
_, err := dbWrapper.Exec(ctx, `UPDATE OR IGNORE `+table+`
|
||||
SET tag_id = ?
|
||||
WHERE tag_id IN `+inBinding+`
|
||||
AND NOT EXISTS(SELECT 1 FROM `+table+` o WHERE o.`+idColumn+` = `+table+`.`+idColumn+` AND o.tag_id = ?)`,
|
||||
@@ -1026,22 +701,22 @@ AND NOT EXISTS(SELECT 1 FROM `+table+` o WHERE o.`+idColumn+` = `+table+`.`+idCo
|
||||
}
|
||||
|
||||
// delete source tag ids from the table where they couldn't be set
|
||||
if _, err := qb.tx.Exec(ctx, `DELETE FROM `+table+` WHERE tag_id IN `+inBinding, srcArgs...); err != nil {
|
||||
if _, err := dbWrapper.Exec(ctx, `DELETE FROM `+table+` WHERE tag_id IN `+inBinding, srcArgs...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err := qb.tx.Exec(ctx, "UPDATE "+sceneMarkerTable+" SET primary_tag_id = ? WHERE primary_tag_id IN "+inBinding, args...)
|
||||
_, err := dbWrapper.Exec(ctx, "UPDATE "+sceneMarkerTable+" SET primary_tag_id = ? WHERE primary_tag_id IN "+inBinding, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = qb.tx.Exec(ctx, "INSERT INTO "+tagAliasesTable+" (tag_id, alias) SELECT ?, name FROM "+tagTable+" WHERE id IN "+inBinding, args...)
|
||||
_, err = dbWrapper.Exec(ctx, "INSERT INTO "+tagAliasesTable+" (tag_id, alias) SELECT ?, name FROM "+tagTable+" WHERE id IN "+inBinding, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = qb.tx.Exec(ctx, "UPDATE "+tagAliasesTable+" SET tag_id = ? WHERE tag_id IN "+inBinding, args...)
|
||||
_, err = dbWrapper.Exec(ctx, "UPDATE "+tagAliasesTable+" SET tag_id = ? WHERE tag_id IN "+inBinding, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1057,8 +732,7 @@ AND NOT EXISTS(SELECT 1 FROM `+table+` o WHERE o.`+idColumn+` = `+table+`.`+idCo
|
||||
}
|
||||
|
||||
func (qb *TagStore) UpdateParentTags(ctx context.Context, tagID int, parentIDs []int) error {
|
||||
tx := qb.tx
|
||||
if _, err := tx.Exec(ctx, "DELETE FROM tags_relations WHERE child_id = ?", tagID); err != nil {
|
||||
if _, err := dbWrapper.Exec(ctx, "DELETE FROM tags_relations WHERE child_id = ?", tagID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1071,7 +745,7 @@ func (qb *TagStore) UpdateParentTags(ctx context.Context, tagID int, parentIDs [
|
||||
}
|
||||
|
||||
query := "INSERT INTO tags_relations (parent_id, child_id) VALUES " + strings.Join(values, ", ")
|
||||
if _, err := tx.Exec(ctx, query, args...); err != nil {
|
||||
if _, err := dbWrapper.Exec(ctx, query, args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1080,8 +754,7 @@ func (qb *TagStore) UpdateParentTags(ctx context.Context, tagID int, parentIDs [
|
||||
}
|
||||
|
||||
func (qb *TagStore) UpdateChildTags(ctx context.Context, tagID int, childIDs []int) error {
|
||||
tx := qb.tx
|
||||
if _, err := tx.Exec(ctx, "DELETE FROM tags_relations WHERE parent_id = ?", tagID); err != nil {
|
||||
if _, err := dbWrapper.Exec(ctx, "DELETE FROM tags_relations WHERE parent_id = ?", tagID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1094,7 +767,7 @@ func (qb *TagStore) UpdateChildTags(ctx context.Context, tagID int, childIDs []i
|
||||
}
|
||||
|
||||
query := "INSERT INTO tags_relations (parent_id, child_id) VALUES " + strings.Join(values, ", ")
|
||||
if _, err := tx.Exec(ctx, query, args...); err != nil {
|
||||
if _, err := dbWrapper.Exec(ctx, query, args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
395
pkg/sqlite/tag_filter.go
Normal file
395
pkg/sqlite/tag_filter.go
Normal file
@@ -0,0 +1,395 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type tagFilterHandler struct {
|
||||
tagFilter *models.TagFilterType
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) validate() error {
|
||||
tagFilter := qb.tagFilter
|
||||
if tagFilter == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := validateFilterCombination(tagFilter.OperatorFilter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if subFilter := tagFilter.SubFilter(); subFilter != nil {
|
||||
sqb := &tagFilterHandler{tagFilter: subFilter}
|
||||
if err := sqb.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) handle(ctx context.Context, f *filterBuilder) {
|
||||
tagFilter := qb.tagFilter
|
||||
if tagFilter == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := qb.validate(); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
sf := tagFilter.SubFilter()
|
||||
if sf != nil {
|
||||
sub := &tagFilterHandler{sf}
|
||||
handleSubFilter(ctx, sub, f, tagFilter.OperatorFilter)
|
||||
}
|
||||
|
||||
f.handleCriterion(ctx, qb.criterionHandler())
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) criterionHandler() criterionHandler {
|
||||
tagFilter := qb.tagFilter
|
||||
return compoundHandler{
|
||||
stringCriterionHandler(tagFilter.Name, tagTable+".name"),
|
||||
qb.aliasCriterionHandler(tagFilter.Aliases),
|
||||
|
||||
boolCriterionHandler(tagFilter.Favorite, tagTable+".favorite", nil),
|
||||
stringCriterionHandler(tagFilter.Description, tagTable+".description"),
|
||||
boolCriterionHandler(tagFilter.IgnoreAutoTag, tagTable+".ignore_auto_tag", nil),
|
||||
|
||||
qb.isMissingCriterionHandler(tagFilter.IsMissing),
|
||||
qb.sceneCountCriterionHandler(tagFilter.SceneCount),
|
||||
qb.imageCountCriterionHandler(tagFilter.ImageCount),
|
||||
qb.galleryCountCriterionHandler(tagFilter.GalleryCount),
|
||||
qb.performerCountCriterionHandler(tagFilter.PerformerCount),
|
||||
qb.markerCountCriterionHandler(tagFilter.MarkerCount),
|
||||
qb.parentsCriterionHandler(tagFilter.Parents),
|
||||
qb.childrenCriterionHandler(tagFilter.Children),
|
||||
qb.parentCountCriterionHandler(tagFilter.ParentCount),
|
||||
qb.childCountCriterionHandler(tagFilter.ChildCount),
|
||||
×tampCriterionHandler{tagFilter.CreatedAt, "tags.created_at", nil},
|
||||
×tampCriterionHandler{tagFilter.UpdatedAt, "tags.updated_at", nil},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "scenes_tags.scene_id",
|
||||
relatedRepo: sceneRepository.repository,
|
||||
relatedHandler: &sceneFilterHandler{tagFilter.ScenesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
tagRepository.scenes.innerJoin(f, "", "tags.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "images_tags.image_id",
|
||||
relatedRepo: imageRepository.repository,
|
||||
relatedHandler: &imageFilterHandler{tagFilter.ImagesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
tagRepository.images.innerJoin(f, "", "tags.id")
|
||||
},
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "galleries_tags.gallery_id",
|
||||
relatedRepo: galleryRepository.repository,
|
||||
relatedHandler: &galleryFilterHandler{tagFilter.GalleriesFilter},
|
||||
joinFn: func(f *filterBuilder) {
|
||||
tagRepository.galleries.innerJoin(f, "", "tags.id")
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) aliasCriterionHandler(alias *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: tagAliasesTable,
|
||||
stringColumn: tagAliasColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
tagRepository.aliases.join(f, "", "tags.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(alias)
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) isMissingCriterionHandler(isMissing *string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "image":
|
||||
f.addWhere("tags.image_blob IS NULL")
|
||||
default:
|
||||
f.addWhere("(tags." + *isMissing + " IS NULL OR TRIM(tags." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) sceneCountCriterionHandler(sceneCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if sceneCount != nil {
|
||||
f.addLeftJoin("scenes_tags", "", "scenes_tags.tag_id = tags.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct scenes_tags.scene_id)", *sceneCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) imageCountCriterionHandler(imageCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if imageCount != nil {
|
||||
f.addLeftJoin("images_tags", "", "images_tags.tag_id = tags.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct images_tags.image_id)", *imageCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) galleryCountCriterionHandler(galleryCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if galleryCount != nil {
|
||||
f.addLeftJoin("galleries_tags", "", "galleries_tags.tag_id = tags.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct galleries_tags.gallery_id)", *galleryCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) performerCountCriterionHandler(performerCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if performerCount != nil {
|
||||
f.addLeftJoin("performers_tags", "", "performers_tags.tag_id = tags.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct performers_tags.performer_id)", *performerCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) markerCountCriterionHandler(markerCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if markerCount != nil {
|
||||
f.addLeftJoin("scene_markers_tags", "", "scene_markers_tags.tag_id = tags.id")
|
||||
f.addLeftJoin("scene_markers", "", "scene_markers_tags.scene_marker_id = scene_markers.id OR scene_markers.primary_tag_id = tags.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct scene_markers.id)", *markerCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) parentsCriterionHandler(criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if criterion != nil {
|
||||
tags := criterion.CombineExcludes()
|
||||
|
||||
// validate the modifier
|
||||
switch tags.Modifier {
|
||||
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
|
||||
// valid
|
||||
default:
|
||||
f.setError(fmt.Errorf("invalid modifier %s for tag parent/children", criterion.Modifier))
|
||||
}
|
||||
|
||||
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if tags.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
f.addLeftJoin("tags_relations", "parent_relations", "tags.id = parent_relations.child_id")
|
||||
|
||||
f.addWhere(fmt.Sprintf("parent_relations.parent_id IS %s NULL", notClause))
|
||||
return
|
||||
}
|
||||
|
||||
if len(tags.Value) == 0 && len(tags.Excludes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(tags.Value) > 0 {
|
||||
var args []interface{}
|
||||
for _, val := range tags.Value {
|
||||
args = append(args, val)
|
||||
}
|
||||
|
||||
depthVal := 0
|
||||
if tags.Depth != nil {
|
||||
depthVal = *tags.Depth
|
||||
}
|
||||
|
||||
var depthCondition string
|
||||
if depthVal != -1 {
|
||||
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
|
||||
}
|
||||
|
||||
query := `parents AS (
|
||||
SELECT parent_id AS root_id, child_id AS item_id, 0 AS depth FROM tags_relations WHERE parent_id IN` + getInBinding(len(tags.Value)) + `
|
||||
UNION
|
||||
SELECT root_id, child_id, depth + 1 FROM tags_relations INNER JOIN parents ON item_id = parent_id ` + depthCondition + `
|
||||
)`
|
||||
|
||||
f.addRecursiveWith(query, args...)
|
||||
|
||||
f.addLeftJoin("parents", "", "parents.item_id = tags.id")
|
||||
|
||||
addHierarchicalConditionClauses(f, tags, "parents", "root_id")
|
||||
}
|
||||
|
||||
if len(tags.Excludes) > 0 {
|
||||
var args []interface{}
|
||||
for _, val := range tags.Excludes {
|
||||
args = append(args, val)
|
||||
}
|
||||
|
||||
depthVal := 0
|
||||
if tags.Depth != nil {
|
||||
depthVal = *tags.Depth
|
||||
}
|
||||
|
||||
var depthCondition string
|
||||
if depthVal != -1 {
|
||||
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
|
||||
}
|
||||
|
||||
query := `parents2 AS (
|
||||
SELECT parent_id AS root_id, child_id AS item_id, 0 AS depth FROM tags_relations WHERE parent_id IN` + getInBinding(len(tags.Excludes)) + `
|
||||
UNION
|
||||
SELECT root_id, child_id, depth + 1 FROM tags_relations INNER JOIN parents2 ON item_id = parent_id ` + depthCondition + `
|
||||
)`
|
||||
|
||||
f.addRecursiveWith(query, args...)
|
||||
|
||||
f.addLeftJoin("parents2", "", "parents2.item_id = tags.id")
|
||||
|
||||
addHierarchicalConditionClauses(f, models.HierarchicalMultiCriterionInput{
|
||||
Value: tags.Excludes,
|
||||
Depth: tags.Depth,
|
||||
Modifier: models.CriterionModifierExcludes,
|
||||
}, "parents2", "root_id")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) childrenCriterionHandler(criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if criterion != nil {
|
||||
tags := criterion.CombineExcludes()
|
||||
|
||||
// validate the modifier
|
||||
switch tags.Modifier {
|
||||
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
|
||||
// valid
|
||||
default:
|
||||
f.setError(fmt.Errorf("invalid modifier %s for tag parent/children", criterion.Modifier))
|
||||
}
|
||||
|
||||
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
|
||||
var notClause string
|
||||
if tags.Modifier == models.CriterionModifierNotNull {
|
||||
notClause = "NOT"
|
||||
}
|
||||
|
||||
f.addLeftJoin("tags_relations", "child_relations", "tags.id = child_relations.parent_id")
|
||||
|
||||
f.addWhere(fmt.Sprintf("child_relations.child_id IS %s NULL", notClause))
|
||||
return
|
||||
}
|
||||
|
||||
if len(tags.Value) == 0 && len(tags.Excludes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(tags.Value) > 0 {
|
||||
var args []interface{}
|
||||
for _, val := range tags.Value {
|
||||
args = append(args, val)
|
||||
}
|
||||
|
||||
depthVal := 0
|
||||
if tags.Depth != nil {
|
||||
depthVal = *tags.Depth
|
||||
}
|
||||
|
||||
var depthCondition string
|
||||
if depthVal != -1 {
|
||||
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
|
||||
}
|
||||
|
||||
query := `children AS (
|
||||
SELECT child_id AS root_id, parent_id AS item_id, 0 AS depth FROM tags_relations WHERE child_id IN` + getInBinding(len(tags.Value)) + `
|
||||
UNION
|
||||
SELECT root_id, parent_id, depth + 1 FROM tags_relations INNER JOIN children ON item_id = child_id ` + depthCondition + `
|
||||
)`
|
||||
|
||||
f.addRecursiveWith(query, args...)
|
||||
|
||||
f.addLeftJoin("children", "", "children.item_id = tags.id")
|
||||
|
||||
addHierarchicalConditionClauses(f, tags, "children", "root_id")
|
||||
}
|
||||
|
||||
if len(tags.Excludes) > 0 {
|
||||
var args []interface{}
|
||||
for _, val := range tags.Excludes {
|
||||
args = append(args, val)
|
||||
}
|
||||
|
||||
depthVal := 0
|
||||
if tags.Depth != nil {
|
||||
depthVal = *tags.Depth
|
||||
}
|
||||
|
||||
var depthCondition string
|
||||
if depthVal != -1 {
|
||||
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
|
||||
}
|
||||
|
||||
query := `children2 AS (
|
||||
SELECT child_id AS root_id, parent_id AS item_id, 0 AS depth FROM tags_relations WHERE child_id IN` + getInBinding(len(tags.Excludes)) + `
|
||||
UNION
|
||||
SELECT root_id, parent_id, depth + 1 FROM tags_relations INNER JOIN children2 ON item_id = child_id ` + depthCondition + `
|
||||
)`
|
||||
|
||||
f.addRecursiveWith(query, args...)
|
||||
|
||||
f.addLeftJoin("children2", "", "children2.item_id = tags.id")
|
||||
|
||||
addHierarchicalConditionClauses(f, models.HierarchicalMultiCriterionInput{
|
||||
Value: tags.Excludes,
|
||||
Depth: tags.Depth,
|
||||
Modifier: models.CriterionModifierExcludes,
|
||||
}, "children2", "root_id")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) parentCountCriterionHandler(parentCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if parentCount != nil {
|
||||
f.addLeftJoin("tags_relations", "parents_count", "parents_count.child_id = tags.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct parents_count.parent_id)", *parentCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) childCountCriterionHandler(childCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if childCount != nil {
|
||||
f.addLeftJoin("tags_relations", "children_count", "children_count.parent_id = tags.id")
|
||||
clause, args := getIntCriterionWhereClause("count(distinct children_count.child_id)", *childCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,9 @@ func logSQL(start time.Time, query string, args ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
type dbWrapper struct{}
|
||||
type dbWrapperType struct{}
|
||||
|
||||
var dbWrapper = dbWrapperType{}
|
||||
|
||||
func sqlError(err error, sql string, args ...interface{}) error {
|
||||
if err == nil {
|
||||
@@ -45,7 +47,7 @@ func sqlError(err error, sql string, args ...interface{}) error {
|
||||
return fmt.Errorf("error executing `%s` [%v]: %w", sql, args, err)
|
||||
}
|
||||
|
||||
func (*dbWrapper) Get(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
|
||||
func (*dbWrapperType) Get(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
|
||||
tx, err := getDBReader(ctx)
|
||||
if err != nil {
|
||||
return sqlError(err, query, args...)
|
||||
@@ -58,7 +60,7 @@ func (*dbWrapper) Get(ctx context.Context, dest interface{}, query string, args
|
||||
return sqlError(err, query, args...)
|
||||
}
|
||||
|
||||
func (*dbWrapper) Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
|
||||
func (*dbWrapperType) Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
|
||||
tx, err := getDBReader(ctx)
|
||||
if err != nil {
|
||||
return sqlError(err, query, args...)
|
||||
@@ -71,7 +73,7 @@ func (*dbWrapper) Select(ctx context.Context, dest interface{}, query string, ar
|
||||
return sqlError(err, query, args...)
|
||||
}
|
||||
|
||||
func (*dbWrapper) Queryx(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) {
|
||||
func (*dbWrapperType) Queryx(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) {
|
||||
tx, err := getDBReader(ctx)
|
||||
if err != nil {
|
||||
return nil, sqlError(err, query, args...)
|
||||
@@ -84,7 +86,7 @@ func (*dbWrapper) Queryx(ctx context.Context, query string, args ...interface{})
|
||||
return ret, sqlError(err, query, args...)
|
||||
}
|
||||
|
||||
func (*dbWrapper) QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) {
|
||||
func (*dbWrapperType) QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) {
|
||||
tx, err := getDBReader(ctx)
|
||||
if err != nil {
|
||||
return nil, sqlError(err, query, args...)
|
||||
@@ -97,7 +99,7 @@ func (*dbWrapper) QueryxContext(ctx context.Context, query string, args ...inter
|
||||
return ret, sqlError(err, query, args...)
|
||||
}
|
||||
|
||||
func (*dbWrapper) NamedExec(ctx context.Context, query string, arg interface{}) (sql.Result, error) {
|
||||
func (*dbWrapperType) NamedExec(ctx context.Context, query string, arg interface{}) (sql.Result, error) {
|
||||
tx, err := getTx(ctx)
|
||||
if err != nil {
|
||||
return nil, sqlError(err, query, arg)
|
||||
@@ -110,7 +112,7 @@ func (*dbWrapper) NamedExec(ctx context.Context, query string, arg interface{})
|
||||
return ret, sqlError(err, query, arg)
|
||||
}
|
||||
|
||||
func (*dbWrapper) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
|
||||
func (*dbWrapperType) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
|
||||
tx, err := getTx(ctx)
|
||||
if err != nil {
|
||||
return nil, sqlError(err, query, args...)
|
||||
@@ -124,7 +126,7 @@ func (*dbWrapper) Exec(ctx context.Context, query string, args ...interface{}) (
|
||||
}
|
||||
|
||||
// Prepare creates a prepared statement.
|
||||
func (*dbWrapper) Prepare(ctx context.Context, query string, args ...interface{}) (*stmt, error) {
|
||||
func (*dbWrapperType) Prepare(ctx context.Context, query string, args ...interface{}) (*stmt, error) {
|
||||
tx, err := getTx(ctx)
|
||||
if err != nil {
|
||||
return nil, sqlError(err, query, args...)
|
||||
@@ -142,7 +144,7 @@ func (*dbWrapper) Prepare(ctx context.Context, query string, args ...interface{}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (*dbWrapper) ExecStmt(ctx context.Context, stmt *stmt, args ...interface{}) (sql.Result, error) {
|
||||
func (*dbWrapperType) ExecStmt(ctx context.Context, stmt *stmt, args ...interface{}) (sql.Result, error) {
|
||||
_, err := getTx(ctx)
|
||||
if err != nil {
|
||||
return nil, sqlError(err, stmt.query, args...)
|
||||
|
||||
Reference in New Issue
Block a user