Related files/folder filter for scenes/images/galleries (#6158)

* Add related files filter to scene filter
* Add files_filter to gallery filter
* Add files_filter to image filter
* Add gallery related folder filter
This commit is contained in:
WithoutPants 2025-11-06 17:25:59 +11:00 committed by GitHub
parent 04fcf6f512
commit a50a0d4289
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 129 additions and 20 deletions

View file

@ -330,6 +330,8 @@ input SceneFilterType {
groups_filter: GroupFilterType groups_filter: GroupFilterType
"Filter by related markers that meet this criteria" "Filter by related markers that meet this criteria"
markers_filter: SceneMarkerFilterType markers_filter: SceneMarkerFilterType
"Filter by related files that meet this criteria"
files_filter: FileFilterType
} }
input MovieFilterType { input MovieFilterType {
@ -534,6 +536,10 @@ input GalleryFilterType {
studios_filter: StudioFilterType studios_filter: StudioFilterType
"Filter by related tags that meet this criteria" "Filter by related tags that meet this criteria"
tags_filter: TagFilterType tags_filter: TagFilterType
"Filter by related files that meet this criteria"
files_filter: FileFilterType
"Filter by related folders that meet this criteria"
folders_filter: FolderFilterType
} }
input TagFilterType { input TagFilterType {
@ -679,6 +685,8 @@ input ImageFilterType {
studios_filter: StudioFilterType studios_filter: StudioFilterType
"Filter by related tags that meet this criteria" "Filter by related tags that meet this criteria"
tags_filter: TagFilterType tags_filter: TagFilterType
"Filter by related files that meet this criteria"
files_filter: FileFilterType
} }
input FileFilterType { input FileFilterType {

View file

@ -59,6 +59,10 @@ type GalleryFilterType struct {
StudiosFilter *StudioFilterType `json:"studios_filter"` StudiosFilter *StudioFilterType `json:"studios_filter"`
// Filter by related tags that meet this criteria // Filter by related tags that meet this criteria
TagsFilter *TagFilterType `json:"tags_filter"` TagsFilter *TagFilterType `json:"tags_filter"`
// Filter by related files that meet this criteria
FilesFilter *FileFilterType `json:"files_filter"`
// Filter by related folders that meet this criteria
FoldersFilter *FolderFilterType `json:"folders_filter"`
// Filter by created at // Filter by created at
CreatedAt *TimestampCriterionInput `json:"created_at"` CreatedAt *TimestampCriterionInput `json:"created_at"`
// Filter by updated at // Filter by updated at

View file

@ -57,6 +57,8 @@ type ImageFilterType struct {
StudiosFilter *StudioFilterType `json:"studios_filter"` StudiosFilter *StudioFilterType `json:"studios_filter"`
// Filter by related tags that meet this criteria // Filter by related tags that meet this criteria
TagsFilter *TagFilterType `json:"tags_filter"` TagsFilter *TagFilterType `json:"tags_filter"`
// Filter by related files that meet this criteria
FilesFilter *FileFilterType `json:"files_filter"`
// Filter by created at // Filter by created at
CreatedAt *TimestampCriterionInput `json:"created_at"` CreatedAt *TimestampCriterionInput `json:"created_at"`
// Filter by updated at // Filter by updated at

View file

@ -111,6 +111,8 @@ type SceneFilterType struct {
MoviesFilter *GroupFilterType `json:"movies_filter"` MoviesFilter *GroupFilterType `json:"movies_filter"`
// Filter by related markers that meet this criteria // Filter by related markers that meet this criteria
MarkersFilter *SceneMarkerFilterType `json:"markers_filter"` MarkersFilter *SceneMarkerFilterType `json:"markers_filter"`
// Filter by related files that meet this criteria
FilesFilter *FileFilterType `json:"files_filter"`
// Filter by created at // Filter by created at
CreatedAt *TimestampCriterionInput `json:"created_at"` CreatedAt *TimestampCriterionInput `json:"created_at"`
// Filter by updated at // Filter by updated at

View file

@ -1041,6 +1041,7 @@ type relatedFilterHandler struct {
relatedRepo repository relatedRepo repository
relatedHandler criterionHandler relatedHandler criterionHandler
joinFn func(f *filterBuilder) joinFn func(f *filterBuilder)
directJoin bool
} }
func (h *relatedFilterHandler) handle(ctx context.Context, f *filterBuilder) { func (h *relatedFilterHandler) handle(ctx context.Context, f *filterBuilder) {
@ -1054,6 +1055,16 @@ func (h *relatedFilterHandler) handle(ctx context.Context, f *filterBuilder) {
return return
} }
if h.joinFn != nil {
h.joinFn(f)
}
if h.directJoin {
// rerun handler using existing filter builder
h.relatedHandler.handle(ctx, f)
return
}
subQuery := h.relatedRepo.newQuery() subQuery := h.relatedRepo.newQuery()
selectIDs(&subQuery, subQuery.repository.tableName) selectIDs(&subQuery, subQuery.repository.tableName)
if err := subQuery.addFilter(ff); err != nil { if err := subQuery.addFilter(ff); err != nil {
@ -1061,9 +1072,5 @@ func (h *relatedFilterHandler) handle(ctx context.Context, f *filterBuilder) {
return return
} }
if h.joinFn != nil {
h.joinFn(f)
}
f.addWhere(fmt.Sprintf("%s IN ("+subQuery.toSQL(false)+")", h.relatedIDCol), subQuery.args...) f.addWhere(fmt.Sprintf("%s IN ("+subQuery.toSQL(false)+")", h.relatedIDCol), subQuery.args...)
} }

View file

@ -285,7 +285,7 @@ type fileRepositoryType struct {
var ( var (
fileRepository = fileRepositoryType{ fileRepository = fileRepositoryType{
repository: repository{ repository: repository{
tableName: sceneTable, tableName: fileTable,
idColumn: idColumn, idColumn: idColumn,
}, },
scenes: joinRepository{ scenes: joinRepository{

View file

@ -10,6 +10,8 @@ import (
type fileFilterHandler struct { type fileFilterHandler struct {
fileFilter *models.FileFilterType fileFilter *models.FileFilterType
// if true, don't allow use of related filters
isRelated bool
} }
func (qb *fileFilterHandler) validate() error { func (qb *fileFilterHandler) validate() error {
@ -22,8 +24,12 @@ func (qb *fileFilterHandler) validate() error {
return err return err
} }
if qb.isRelated && (fileFilter.ScenesFilter != nil || fileFilter.ImagesFilter != nil || fileFilter.GalleriesFilter != nil) {
return fmt.Errorf("cannot use related filters inside a related filter")
}
if subFilter := fileFilter.SubFilter(); subFilter != nil { if subFilter := fileFilter.SubFilter(); subFilter != nil {
sqb := &fileFilterHandler{fileFilter: subFilter} sqb := &fileFilterHandler{fileFilter: subFilter, isRelated: qb.isRelated}
if err := sqb.validate(); err != nil { if err := sqb.validate(); err != nil {
return err return err
} }
@ -45,7 +51,7 @@ func (qb *fileFilterHandler) handle(ctx context.Context, f *filterBuilder) {
sf := fileFilter.SubFilter() sf := fileFilter.SubFilter()
if sf != nil { if sf != nil {
sub := &fileFilterHandler{sf} sub := &fileFilterHandler{sf, qb.isRelated}
handleSubFilter(ctx, sub, f, fileFilter.OperatorFilter) handleSubFilter(ctx, sub, f, fileFilter.OperatorFilter)
} }

View file

@ -9,6 +9,8 @@ import (
type folderFilterHandler struct { type folderFilterHandler struct {
folderFilter *models.FolderFilterType folderFilter *models.FolderFilterType
table sqlTable
isRelated bool
} }
func (qb *folderFilterHandler) validate() error { func (qb *folderFilterHandler) validate() error {
@ -21,8 +23,12 @@ func (qb *folderFilterHandler) validate() error {
return err return err
} }
if qb.isRelated && (folderFilter.GalleriesFilter != nil) {
return fmt.Errorf("cannot use related filters inside a related filter")
}
if subFilter := folderFilter.SubFilter(); subFilter != nil { if subFilter := folderFilter.SubFilter(); subFilter != nil {
sqb := &folderFilterHandler{folderFilter: subFilter} sqb := &folderFilterHandler{folderFilter: subFilter, isRelated: qb.isRelated}
if err := sqb.validate(); err != nil { if err := sqb.validate(); err != nil {
return err return err
} }
@ -44,7 +50,7 @@ func (qb *folderFilterHandler) handle(ctx context.Context, f *filterBuilder) {
sf := folderFilter.SubFilter() sf := folderFilter.SubFilter()
if sf != nil { if sf != nil {
sub := &folderFilterHandler{sf} sub := &folderFilterHandler{folderFilter: sf, table: qb.table}
handleSubFilter(ctx, sub, f, folderFilter.OperatorFilter) handleSubFilter(ctx, sub, f, folderFilter.OperatorFilter)
} }
@ -52,25 +58,29 @@ func (qb *folderFilterHandler) handle(ctx context.Context, f *filterBuilder) {
} }
func (qb *folderFilterHandler) criterionHandler() criterionHandler { func (qb *folderFilterHandler) criterionHandler() criterionHandler {
if qb.table == "" {
qb.table = folderTable
}
folderFilter := qb.folderFilter folderFilter := qb.folderFilter
return compoundHandler{ return compoundHandler{
stringCriterionHandler(folderFilter.Path, "folders.path"), stringCriterionHandler(folderFilter.Path, qb.table.Col("path")),
&timestampCriterionHandler{folderFilter.ModTime, "folders.mod_time", nil}, &timestampCriterionHandler{folderFilter.ModTime, qb.table.Col("mod_time"), nil},
qb.parentFolderCriterionHandler(folderFilter.ParentFolder), qb.parentFolderCriterionHandler(folderFilter.ParentFolder),
qb.zipFileCriterionHandler(folderFilter.ZipFile), qb.zipFileCriterionHandler(folderFilter.ZipFile),
qb.galleryCountCriterionHandler(folderFilter.GalleryCount), qb.galleryCountCriterionHandler(folderFilter.GalleryCount),
&timestampCriterionHandler{folderFilter.CreatedAt, "folders.created_at", nil}, &timestampCriterionHandler{folderFilter.CreatedAt, qb.table.Col("created_at"), nil},
&timestampCriterionHandler{folderFilter.UpdatedAt, "folders.updated_at", nil}, &timestampCriterionHandler{folderFilter.UpdatedAt, qb.table.Col("updated_at"), nil},
&relatedFilterHandler{ &relatedFilterHandler{
relatedIDCol: "galleries.id", relatedIDCol: qb.table.Col("id"),
relatedRepo: galleryRepository.repository, relatedRepo: galleryRepository.repository,
relatedHandler: &galleryFilterHandler{folderFilter.GalleriesFilter}, relatedHandler: &galleryFilterHandler{folderFilter.GalleriesFilter},
joinFn: func(f *filterBuilder) { joinFn: func(f *filterBuilder) {
folderRepository.galleries.innerJoin(f, "", "folders.id") folderRepository.galleries.innerJoin(f, "", qb.table.Col("id"))
}, },
}, },
} }
@ -85,7 +95,7 @@ func (qb *folderFilterHandler) zipFileCriterionHandler(criterion *models.MultiCr
notClause = "NOT" notClause = "NOT"
} }
f.addWhere(fmt.Sprintf("folders.zip_file_id IS %s NULL", notClause)) f.addWhere(fmt.Sprintf("%s.zip_file_id IS %s NULL", qb.table.Name(), notClause))
return return
} }
@ -102,9 +112,9 @@ func (qb *folderFilterHandler) zipFileCriterionHandler(criterion *models.MultiCr
havingClause := "" havingClause := ""
switch criterion.Modifier { switch criterion.Modifier {
case models.CriterionModifierIncludes: case models.CriterionModifierIncludes:
whereClause = "folders.zip_file_id IN " + getInBinding(len(criterion.Value)) whereClause = fmt.Sprintf("%s.zip_file_id IN %s", qb.table.Name(), getInBinding(len(criterion.Value)))
case models.CriterionModifierExcludes: case models.CriterionModifierExcludes:
whereClause = "folders.zip_file_id NOT IN " + getInBinding(len(criterion.Value)) whereClause = fmt.Sprintf("%s.zip_file_id NOT IN %s", qb.table.Name(), getInBinding(len(criterion.Value)))
} }
f.addWhere(whereClause, args...) f.addWhere(whereClause, args...)
@ -128,8 +138,8 @@ func (qb *folderFilterHandler) parentFolderCriterionHandler(folder *models.Hiera
} }
hh := hierarchicalMultiCriterionHandlerBuilder{ hh := hierarchicalMultiCriterionHandlerBuilder{
primaryTable: folderTable, primaryTable: qb.table.Name(),
foreignTable: folderTable, foreignTable: qb.table.Name(),
foreignFK: "parent_folder_id", foreignFK: "parent_folder_id",
parentFK: "parent_folder_id", parentFK: "parent_folder_id",
} }

View file

@ -146,6 +146,36 @@ func (qb *galleryFilterHandler) criterionHandler() criterionHandler {
galleryRepository.tags.innerJoin(f, "gallery_tag", "galleries.id") galleryRepository.tags.innerJoin(f, "gallery_tag", "galleries.id")
}, },
}, },
&relatedFilterHandler{
relatedIDCol: "files.id",
relatedRepo: fileRepository.repository,
relatedHandler: &fileFilterHandler{
fileFilter: filter.FilesFilter,
isRelated: true,
},
joinFn: func(f *filterBuilder) {
galleryRepository.addFilesTable(f)
galleryRepository.addFoldersTable(f)
},
// don't use a subquery; join directly
directJoin: true,
},
&relatedFilterHandler{
relatedIDCol: "gallery_folder.id",
relatedRepo: folderRepository.repository,
relatedHandler: &folderFilterHandler{
folderFilter: filter.FoldersFilter,
table: "gallery_folder",
isRelated: true,
},
joinFn: func(f *filterBuilder) {
f.addLeftJoin(folderTable, "gallery_folder", "galleries.folder_id = gallery_folder.id")
},
// don't use a subquery; join directly
directJoin: true,
},
} }
} }

View file

@ -123,6 +123,21 @@ func (qb *imageFilterHandler) criterionHandler() criterionHandler {
imageRepository.tags.innerJoin(f, "image_tag", "images.id") imageRepository.tags.innerJoin(f, "image_tag", "images.id")
}, },
}, },
&relatedFilterHandler{
relatedIDCol: "files.id",
relatedRepo: fileRepository.repository,
relatedHandler: &fileFilterHandler{
fileFilter: imageFilter.FilesFilter,
isRelated: true,
},
joinFn: func(f *filterBuilder) {
imageRepository.addFilesTable(f)
imageRepository.addFoldersTable(f)
},
// don't use a subquery; join directly
directJoin: true,
},
} }
} }

View file

@ -202,6 +202,21 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
}, },
}, },
&relatedFilterHandler{
relatedIDCol: "files.id",
relatedRepo: fileRepository.repository,
relatedHandler: &fileFilterHandler{
fileFilter: sceneFilter.FilesFilter,
isRelated: true,
},
joinFn: func(f *filterBuilder) {
qb.addFilesTable(f)
qb.addFoldersTable(f)
},
// don't use a subquery; join directly
directJoin: true,
},
&relatedFilterHandler{ &relatedFilterHandler{
relatedIDCol: "scene_markers.id", relatedIDCol: "scene_markers.id",
relatedRepo: sceneMarkerRepository.repository, relatedRepo: sceneMarkerRepository.repository,

View file

@ -362,3 +362,13 @@ func coalesce(column string) string {
func like(v string) string { func like(v string) string {
return "%" + v + "%" return "%" + v + "%"
} }
type sqlTable string
func (t sqlTable) Name() string {
return string(t)
}
func (t sqlTable) Col(n string) string {
return fmt.Sprintf("%s.%s", string(t), n)
}