Autotag support for images and galleries (#1345)

* Add compound queries for images and galleries
* Implement image and gallery auto tagging
This commit is contained in:
WithoutPants
2021-05-03 13:09:46 +10:00
committed by GitHub
parent 2c52fd711b
commit a3609079bb
27 changed files with 2910 additions and 521 deletions

View File

@@ -12,35 +12,6 @@ const imageIDColumn = "image_id"
const performersImagesTable = "performers_images"
const imagesTagsTable = "images_tags"
var imagesForPerformerQuery = selectAll(imageTable) + `
LEFT JOIN performers_images as performers_join on performers_join.image_id = images.id
WHERE performers_join.performer_id = ?
GROUP BY images.id
`
var countImagesForPerformerQuery = `
SELECT performer_id FROM performers_images as performers_join
WHERE performer_id = ?
GROUP BY image_id
`
var imagesForStudioQuery = selectAll(imageTable) + `
JOIN studios ON studios.id = images.studio_id
WHERE studios.id = ?
GROUP BY images.id
`
var imagesForMovieQuery = selectAll(imageTable) + `
LEFT JOIN movies_images as movies_join on movies_join.image_id = images.id
WHERE movies_join.movie_id = ?
GROUP BY images.id
`
var countImagesForTagQuery = `
SELECT tag_id AS id FROM images_tags
WHERE images_tags.tag_id = ?
GROUP BY images_tags.image_id
`
var imagesForGalleryQuery = selectAll(imageTable) + `
LEFT JOIN galleries_images as galleries_join on galleries_join.image_id = images.id
WHERE galleries_join.gallery_id = ?
@@ -216,7 +187,69 @@ func (qb *imageQueryBuilder) All() ([]*models.Image, error) {
return qb.queryImages(selectAll(imageTable)+qb.getImageSort(nil), nil)
}
func (qb *imageQueryBuilder) makeQuery(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) queryBuilder {
func (qb *imageQueryBuilder) 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 *imageQueryBuilder) makeFilter(imageFilter *models.ImageFilterType) *filterBuilder {
query := &filterBuilder{}
if imageFilter.And != nil {
query.and(qb.makeFilter(imageFilter.And))
}
if imageFilter.Or != nil {
query.or(qb.makeFilter(imageFilter.Or))
}
if imageFilter.Not != nil {
query.not(qb.makeFilter(imageFilter.Not))
}
query.handleCriterionFunc(stringCriterionHandler(imageFilter.Path, "images.path"))
query.handleCriterionFunc(intCriterionHandler(imageFilter.Rating, "images.rating"))
query.handleCriterionFunc(intCriterionHandler(imageFilter.OCounter, "images.o_counter"))
query.handleCriterionFunc(boolCriterionHandler(imageFilter.Organized, "images.organized"))
query.handleCriterionFunc(resolutionCriterionHandler(imageFilter.Resolution, "images.height", "images.width"))
query.handleCriterionFunc(imageIsMissingCriterionHandler(qb, imageFilter.IsMissing))
query.handleCriterionFunc(imageTagsCriterionHandler(qb, imageFilter.Tags))
query.handleCriterionFunc(imageTagCountCriterionHandler(qb, imageFilter.TagCount))
query.handleCriterionFunc(imageGalleriesCriterionHandler(qb, imageFilter.Galleries))
query.handleCriterionFunc(imagePerformersCriterionHandler(qb, imageFilter.Performers))
query.handleCriterionFunc(imagePerformerCountCriterionHandler(qb, imageFilter.PerformerCount))
query.handleCriterionFunc(imageStudioCriterionHandler(qb, imageFilter.Studios))
query.handleCriterionFunc(imagePerformerTagsCriterionHandler(qb, imageFilter.PerformerTags))
return query
}
func (qb *imageQueryBuilder) makeQuery(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
if imageFilter == nil {
imageFilter = &models.ImageFilterType{}
}
@@ -227,12 +260,6 @@ func (qb *imageQueryBuilder) makeQuery(imageFilter *models.ImageFilterType, find
query := qb.newQuery()
query.body = selectDistinctIDs(imageTable)
query.body += `
left join performers_images as performers_join on performers_join.image_id = images.id
left join studios as studio on studio.id = images.studio_id
left join images_tags as tags_join on tags_join.image_id = images.id
left join galleries_images as galleries_join on galleries_join.image_id = images.id
`
if q := findFilter.Q; q != nil && *q != "" {
searchColumns := []string{"images.title", "images.path", "images.checksum"}
@@ -241,154 +268,23 @@ func (qb *imageQueryBuilder) makeQuery(imageFilter *models.ImageFilterType, find
query.addArg(thisArgs...)
}
query.handleStringCriterionInput(imageFilter.Path, "images.path")
if rating := imageFilter.Rating; rating != nil {
clause, count := getIntCriterionWhereClause("images.rating", *imageFilter.Rating)
query.addWhere(clause)
if count == 1 {
query.addArg(imageFilter.Rating.Value)
}
if err := qb.validateFilter(imageFilter); err != nil {
return nil, err
}
filter := qb.makeFilter(imageFilter)
if oCounter := imageFilter.OCounter; oCounter != nil {
clause, count := getIntCriterionWhereClause("images.o_counter", *imageFilter.OCounter)
query.addWhere(clause)
if count == 1 {
query.addArg(imageFilter.OCounter.Value)
}
}
if Organized := imageFilter.Organized; Organized != nil {
var organized string
if *Organized == true {
organized = "1"
} else {
organized = "0"
}
query.addWhere("images.organized = " + organized)
}
if resolutionFilter := imageFilter.Resolution; resolutionFilter != nil {
if resolution := resolutionFilter.String(); resolutionFilter.IsValid() {
switch resolution {
case "VERY_LOW":
query.addWhere("MIN(images.height, images.width) < 240")
case "LOW":
query.addWhere("(MIN(images.height, images.width) >= 240 AND MIN(images.height, images.width) < 360)")
case "R360P":
query.addWhere("(MIN(images.height, images.width) >= 360 AND MIN(images.height, images.width) < 480)")
case "STANDARD":
query.addWhere("(MIN(images.height, images.width) >= 480 AND MIN(images.height, images.width) < 540)")
case "WEB_HD":
query.addWhere("(MIN(images.height, images.width) >= 540 AND MIN(images.height, images.width) < 720)")
case "STANDARD_HD":
query.addWhere("(MIN(images.height, images.width) >= 720 AND MIN(images.height, images.width) < 1080)")
case "FULL_HD":
query.addWhere("(MIN(images.height, images.width) >= 1080 AND MIN(images.height, images.width) < 1440)")
case "QUAD_HD":
query.addWhere("(MIN(images.height, images.width) >= 1440 AND MIN(images.height, images.width) < 1920)")
case "VR_HD":
query.addWhere("(MIN(images.height, images.width) >= 1920 AND MIN(images.height, images.width) < 2160)")
case "FOUR_K":
query.addWhere("(MIN(images.height, images.width) >= 2160 AND MIN(images.height, images.width) < 2880)")
case "FIVE_K":
query.addWhere("(MIN(images.height, images.width) >= 2880 AND MIN(images.height, images.width) < 3384)")
case "SIX_K":
query.addWhere("(MIN(images.height, images.width) >= 3384 AND MIN(images.height, images.width) < 4320)")
case "EIGHT_K":
query.addWhere("MIN(images.height, images.width) >= 4320")
}
}
}
if isMissingFilter := imageFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
switch *isMissingFilter {
case "studio":
query.addWhere("images.studio_id IS NULL")
case "performers":
query.addWhere("performers_join.image_id IS NULL")
case "galleries":
query.addWhere("galleries_join.image_id IS NULL")
case "tags":
query.addWhere("tags_join.image_id IS NULL")
default:
query.addWhere("(images." + *isMissingFilter + " IS NULL OR TRIM(images." + *isMissingFilter + ") = '')")
}
}
if tagsFilter := imageFilter.Tags; tagsFilter != nil && len(tagsFilter.Value) > 0 {
for _, tagID := range tagsFilter.Value {
query.addArg(tagID)
}
query.body += " LEFT JOIN tags on tags_join.tag_id = tags.id"
whereClause, havingClause := getMultiCriterionClause("images", "tags", "images_tags", "image_id", "tag_id", tagsFilter)
query.addWhere(whereClause)
query.addHaving(havingClause)
}
if tagCountFilter := imageFilter.TagCount; tagCountFilter != nil {
clause, count := getCountCriterionClause(imageTable, imagesTagsTable, imageIDColumn, *tagCountFilter)
if count == 1 {
query.addArg(tagCountFilter.Value)
}
query.addWhere(clause)
}
if galleriesFilter := imageFilter.Galleries; galleriesFilter != nil && len(galleriesFilter.Value) > 0 {
for _, galleryID := range galleriesFilter.Value {
query.addArg(galleryID)
}
query.body += " LEFT JOIN galleries ON galleries_join.gallery_id = galleries.id"
whereClause, havingClause := getMultiCriterionClause("images", "galleries", "galleries_images", "image_id", "gallery_id", galleriesFilter)
query.addWhere(whereClause)
query.addHaving(havingClause)
}
if performersFilter := imageFilter.Performers; performersFilter != nil && len(performersFilter.Value) > 0 {
for _, performerID := range performersFilter.Value {
query.addArg(performerID)
}
query.body += " LEFT JOIN performers ON performers_join.performer_id = performers.id"
whereClause, havingClause := getMultiCriterionClause("images", "performers", "performers_images", "image_id", "performer_id", performersFilter)
query.addWhere(whereClause)
query.addHaving(havingClause)
}
if performerCountFilter := imageFilter.PerformerCount; performerCountFilter != nil {
clause, count := getCountCriterionClause(imageTable, performersImagesTable, imageIDColumn, *performerCountFilter)
if count == 1 {
query.addArg(performerCountFilter.Value)
}
query.addWhere(clause)
}
if studiosFilter := imageFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 {
for _, studioID := range studiosFilter.Value {
query.addArg(studioID)
}
whereClause, havingClause := getMultiCriterionClause("images", "studio", "", "", "studio_id", studiosFilter)
query.addWhere(whereClause)
query.addHaving(havingClause)
}
handleImagePerformerTagsCriterion(&query, imageFilter.PerformerTags)
query.addFilter(filter)
query.sortAndPagination = qb.getImageSort(findFilter) + getPagination(findFilter)
return query
return &query, nil
}
func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) ([]*models.Image, int, error) {
query := qb.makeQuery(imageFilter, findFilter)
query, err := qb.makeQuery(imageFilter, findFilter)
if err != nil {
return nil, 0, err
}
idsResult, countResult, err := query.executeFind()
if err != nil {
@@ -409,32 +305,131 @@ func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilt
}
func (qb *imageQueryBuilder) QueryCount(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) (int, error) {
query := qb.makeQuery(imageFilter, findFilter)
query, err := qb.makeQuery(imageFilter, findFilter)
if err != nil {
return 0, err
}
return query.executeCount()
}
func handleImagePerformerTagsCriterion(query *queryBuilder, performerTagsFilter *models.MultiCriterionInput) {
if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 {
for _, tagID := range performerTagsFilter.Value {
query.addArg(tagID)
func imageIsMissingCriterionHandler(qb *imageQueryBuilder, isMissing *string) criterionHandlerFunc {
return func(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 + ") = '')")
}
}
}
}
query.body += " LEFT JOIN performers_tags AS performer_tags_join on performers_join.performer_id = performer_tags_join.performer_id"
func (qb *imageQueryBuilder) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
return multiCriterionHandlerBuilder{
primaryTable: imageTable,
foreignTable: foreignTable,
joinTable: joinTable,
primaryFK: imageIDColumn,
foreignFK: foreignFK,
addJoinsFunc: addJoinsFunc,
}
}
if performerTagsFilter.Modifier == models.CriterionModifierIncludes {
// includes any of the provided ids
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
} else if performerTagsFilter.Modifier == models.CriterionModifierIncludesAll {
// includes all of the provided ids
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
query.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value)))
} else if performerTagsFilter.Modifier == models.CriterionModifierExcludes {
query.addWhere(fmt.Sprintf(`not exists
(select performers_images.performer_id from performers_images
left join performers_tags on performers_tags.performer_id = performers_images.performer_id where
performers_images.image_id = images.id AND
performers_tags.tag_id in %s)`, getInBinding(len(performerTagsFilter.Value))))
func imageTagsCriterionHandler(qb *imageQueryBuilder, tags *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
qb.tagsRepository().join(f, "tags_join", "images.id")
f.addJoin(tagTable, "", "tags_join.tag_id = tags.id")
}
h := qb.getMultiCriterionHandlerBuilder(tagTable, imagesTagsTable, tagIDColumn, addJoinsFunc)
return h.handler(tags)
}
func imageTagCountCriterionHandler(qb *imageQueryBuilder, tagCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: imageTable,
joinTable: imagesTagsTable,
primaryFK: imageIDColumn,
}
return h.handler(tagCount)
}
func imageGalleriesCriterionHandler(qb *imageQueryBuilder, galleries *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
qb.galleriesRepository().join(f, "galleries_join", "images.id")
f.addJoin(galleryTable, "", "galleries_join.gallery_id = galleries.id")
}
h := qb.getMultiCriterionHandlerBuilder(galleryTable, galleriesImagesTable, galleryIDColumn, addJoinsFunc)
return h.handler(galleries)
}
func imagePerformersCriterionHandler(qb *imageQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
qb.performersRepository().join(f, "performers_join", "images.id")
f.addJoin(performerTable, "", "performers_join.performer_id = performers.id")
}
h := qb.getMultiCriterionHandlerBuilder(performerTable, performersImagesTable, performerIDColumn, addJoinsFunc)
return h.handler(performers)
}
func imagePerformerCountCriterionHandler(qb *imageQueryBuilder, performerCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: imageTable,
joinTable: performersImagesTable,
primaryFK: imageIDColumn,
}
return h.handler(performerCount)
}
func imageStudioCriterionHandler(qb *imageQueryBuilder, studios *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
f.addJoin(studioTable, "studio", "studio.id = images.studio_id")
}
h := qb.getMultiCriterionHandlerBuilder("studio", "", studioIDColumn, addJoinsFunc)
return h.handler(studios)
}
func imagePerformerTagsCriterionHandler(qb *imageQueryBuilder, performerTagsFilter *models.MultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) {
if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 {
qb.performersRepository().join(f, "performers_join", "images.id")
f.addJoin("performers_tags", "performer_tags_join", "performers_join.performer_id = performer_tags_join.performer_id")
var args []interface{}
for _, tagID := range performerTagsFilter.Value {
args = append(args, tagID)
}
if performerTagsFilter.Modifier == models.CriterionModifierIncludes {
// includes any of the provided ids
f.addWhere("performer_tags_join.tag_id IN "+getInBinding(len(performerTagsFilter.Value)), args...)
} else if performerTagsFilter.Modifier == models.CriterionModifierIncludesAll {
// includes all of the provided ids
f.addWhere("performer_tags_join.tag_id IN "+getInBinding(len(performerTagsFilter.Value)), args...)
f.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value)))
} else if performerTagsFilter.Modifier == models.CriterionModifierExcludes {
f.addWhere(fmt.Sprintf(`not exists
(select performers_images.performer_id from performers_images
left join performers_tags on performers_tags.performer_id = performers_images.performer_id where
performers_images.image_id = images.id AND
performers_tags.tag_id in %s)`, getInBinding(len(performerTagsFilter.Value))), args...)
}
}
}
}