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
"Filter by related markers that meet this criteria"
markers_filter: SceneMarkerFilterType
"Filter by related files that meet this criteria"
files_filter: FileFilterType
}
input MovieFilterType {
@ -534,6 +536,10 @@ input GalleryFilterType {
studios_filter: StudioFilterType
"Filter by related tags that meet this criteria"
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 {
@ -679,6 +685,8 @@ input ImageFilterType {
studios_filter: StudioFilterType
"Filter by related tags that meet this criteria"
tags_filter: TagFilterType
"Filter by related files that meet this criteria"
files_filter: FileFilterType
}
input FileFilterType {

View file

@ -59,6 +59,10 @@ type GalleryFilterType struct {
StudiosFilter *StudioFilterType `json:"studios_filter"`
// Filter by related tags that meet this criteria
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
CreatedAt *TimestampCriterionInput `json:"created_at"`
// Filter by updated at

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,6 +9,8 @@ import (
type folderFilterHandler struct {
folderFilter *models.FolderFilterType
table sqlTable
isRelated bool
}
func (qb *folderFilterHandler) validate() error {
@ -21,8 +23,12 @@ func (qb *folderFilterHandler) validate() error {
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 {
sqb := &folderFilterHandler{folderFilter: subFilter}
sqb := &folderFilterHandler{folderFilter: subFilter, isRelated: qb.isRelated}
if err := sqb.validate(); err != nil {
return err
}
@ -44,7 +50,7 @@ func (qb *folderFilterHandler) handle(ctx context.Context, f *filterBuilder) {
sf := folderFilter.SubFilter()
if sf != nil {
sub := &folderFilterHandler{sf}
sub := &folderFilterHandler{folderFilter: sf, table: qb.table}
handleSubFilter(ctx, sub, f, folderFilter.OperatorFilter)
}
@ -52,25 +58,29 @@ func (qb *folderFilterHandler) handle(ctx context.Context, f *filterBuilder) {
}
func (qb *folderFilterHandler) criterionHandler() criterionHandler {
if qb.table == "" {
qb.table = folderTable
}
folderFilter := qb.folderFilter
return compoundHandler{
stringCriterionHandler(folderFilter.Path, "folders.path"),
&timestampCriterionHandler{folderFilter.ModTime, "folders.mod_time", nil},
stringCriterionHandler(folderFilter.Path, qb.table.Col("path")),
&timestampCriterionHandler{folderFilter.ModTime, qb.table.Col("mod_time"), nil},
qb.parentFolderCriterionHandler(folderFilter.ParentFolder),
qb.zipFileCriterionHandler(folderFilter.ZipFile),
qb.galleryCountCriterionHandler(folderFilter.GalleryCount),
&timestampCriterionHandler{folderFilter.CreatedAt, "folders.created_at", nil},
&timestampCriterionHandler{folderFilter.UpdatedAt, "folders.updated_at", nil},
&timestampCriterionHandler{folderFilter.CreatedAt, qb.table.Col("created_at"), nil},
&timestampCriterionHandler{folderFilter.UpdatedAt, qb.table.Col("updated_at"), nil},
&relatedFilterHandler{
relatedIDCol: "galleries.id",
relatedIDCol: qb.table.Col("id"),
relatedRepo: galleryRepository.repository,
relatedHandler: &galleryFilterHandler{folderFilter.GalleriesFilter},
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"
}
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
}
@ -102,9 +112,9 @@ func (qb *folderFilterHandler) zipFileCriterionHandler(criterion *models.MultiCr
havingClause := ""
switch criterion.Modifier {
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:
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...)
@ -128,8 +138,8 @@ func (qb *folderFilterHandler) parentFolderCriterionHandler(folder *models.Hiera
}
hh := hierarchicalMultiCriterionHandlerBuilder{
primaryTable: folderTable,
foreignTable: folderTable,
primaryTable: qb.table.Name(),
foreignTable: qb.table.Name(),
foreignFK: "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")
},
},
&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")
},
},
&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{
relatedIDCol: "scene_markers.id",
relatedRepo: sceneMarkerRepository.repository,

View file

@ -362,3 +362,13 @@ func coalesce(column string) string {
func like(v string) string {
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)
}