mirror of
https://github.com/stashapp/stash.git
synced 2025-12-09 01:44:52 +01:00
* Add basic support for hierarchical filters Add a new `hierarchicalMultiCriterionHandlerBuilder` filter type which can / will be used for filtering hierarchical things like the parent/child relation of the studios. On the frontend side a new IHierarchicalLabeledIdCriterion criterion type has been added to accompany this new filter type. * Refactor movieQueryBuilder to use filterBuilder Refactor the movieQueryBuilder to use the filterBuilder just as scene, image and gallery as well. * Support specifying depth for studios filter Add an optional depth field to the studios filter for scenes, images, galleries and movies. When specified that number of included (grant)children are shown as well. In other words: this adds support for showing scenes set to child studios when searching on the parent studio. Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
331 lines
11 KiB
Go
331 lines
11 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"github.com/stashapp/stash/pkg/database"
|
|
"github.com/stashapp/stash/pkg/models"
|
|
)
|
|
|
|
const sceneMarkerTable = "scene_markers"
|
|
|
|
const countSceneMarkersForTagQuery = `
|
|
SELECT scene_markers.id FROM scene_markers
|
|
LEFT JOIN scene_markers_tags as tags_join on tags_join.scene_marker_id = scene_markers.id
|
|
WHERE tags_join.tag_id = ? OR scene_markers.primary_tag_id = ?
|
|
GROUP BY scene_markers.id
|
|
`
|
|
|
|
type sceneMarkerQueryBuilder struct {
|
|
repository
|
|
}
|
|
|
|
func NewSceneMarkerReaderWriter(tx dbi) *sceneMarkerQueryBuilder {
|
|
return &sceneMarkerQueryBuilder{
|
|
repository{
|
|
tx: tx,
|
|
tableName: sceneMarkerTable,
|
|
idColumn: idColumn,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) Create(newObject models.SceneMarker) (*models.SceneMarker, error) {
|
|
var ret models.SceneMarker
|
|
if err := qb.insertObject(newObject, &ret); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ret, nil
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) Update(updatedObject models.SceneMarker) (*models.SceneMarker, error) {
|
|
const partial = false
|
|
if err := qb.update(updatedObject.ID, updatedObject, partial); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var ret models.SceneMarker
|
|
if err := qb.get(updatedObject.ID, &ret); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ret, nil
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) Destroy(id int) error {
|
|
return qb.destroyExisting([]int{id})
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) Find(id int) (*models.SceneMarker, error) {
|
|
query := "SELECT * FROM scene_markers WHERE id = ? LIMIT 1"
|
|
args := []interface{}{id}
|
|
results, err := qb.querySceneMarkers(query, args)
|
|
if err != nil || len(results) < 1 {
|
|
return nil, err
|
|
}
|
|
return results[0], nil
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) FindMany(ids []int) ([]*models.SceneMarker, error) {
|
|
var markers []*models.SceneMarker
|
|
for _, id := range ids {
|
|
marker, err := qb.Find(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if marker == nil {
|
|
return nil, fmt.Errorf("scene marker with id %d not found", id)
|
|
}
|
|
|
|
markers = append(markers, marker)
|
|
}
|
|
|
|
return markers, nil
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) FindBySceneID(sceneID int) ([]*models.SceneMarker, error) {
|
|
query := `
|
|
SELECT scene_markers.* FROM scene_markers
|
|
WHERE scene_markers.scene_id = ?
|
|
GROUP BY scene_markers.id
|
|
ORDER BY scene_markers.seconds ASC
|
|
`
|
|
args := []interface{}{sceneID}
|
|
return qb.querySceneMarkers(query, args)
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) CountByTagID(tagID int) (int, error) {
|
|
args := []interface{}{tagID, tagID}
|
|
return qb.runCountQuery(qb.buildCountQuery(countSceneMarkersForTagQuery), args)
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) GetMarkerStrings(q *string, sort *string) ([]*models.MarkerStringsResultType, error) {
|
|
query := "SELECT count(*) as `count`, scene_markers.id as id, scene_markers.title as title FROM scene_markers"
|
|
if q != nil {
|
|
query = query + " WHERE title LIKE '%" + *q + "%'"
|
|
}
|
|
query = query + " GROUP BY title"
|
|
if sort != nil && *sort == "count" {
|
|
query = query + " ORDER BY `count` DESC"
|
|
} else {
|
|
query = query + " ORDER BY title ASC"
|
|
}
|
|
var args []interface{}
|
|
return qb.queryMarkerStringsResultType(query, args)
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) Wall(q *string) ([]*models.SceneMarker, error) {
|
|
s := ""
|
|
if q != nil {
|
|
s = *q
|
|
}
|
|
query := "SELECT scene_markers.* FROM scene_markers WHERE scene_markers.title LIKE '%" + s + "%' ORDER BY RANDOM() LIMIT 80"
|
|
return qb.querySceneMarkers(query, nil)
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) Query(sceneMarkerFilter *models.SceneMarkerFilterType, findFilter *models.FindFilterType) ([]*models.SceneMarker, int, error) {
|
|
if sceneMarkerFilter == nil {
|
|
sceneMarkerFilter = &models.SceneMarkerFilterType{}
|
|
}
|
|
if findFilter == nil {
|
|
findFilter = &models.FindFilterType{}
|
|
}
|
|
|
|
var whereClauses []string
|
|
var havingClauses []string
|
|
var args []interface{}
|
|
body := selectDistinctIDs("scene_markers")
|
|
body = body + `
|
|
left join tags as primary_tag on primary_tag.id = scene_markers.primary_tag_id
|
|
left join scenes as scene on scene.id = scene_markers.scene_id
|
|
left join scene_markers_tags as tags_join on tags_join.scene_marker_id = scene_markers.id
|
|
left join tags on tags_join.tag_id = tags.id
|
|
`
|
|
|
|
if tagsFilter := sceneMarkerFilter.Tags; tagsFilter != nil && len(tagsFilter.Value) > 0 {
|
|
//select `scene_markers`.* from `scene_markers`
|
|
//left join `tags` as `primary_tags_join`
|
|
// on `primary_tags_join`.`id` = `scene_markers`.`primary_tag_id`
|
|
// and `primary_tags_join`.`id` in ('3', '37', '9', '89')
|
|
//left join `scene_markers_tags` as `tags_join`
|
|
// on `tags_join`.`scene_marker_id` = `scene_markers`.`id`
|
|
// and `tags_join`.`tag_id` in ('3', '37', '9', '89')
|
|
//group by `scene_markers`.`id`
|
|
//having ((count(distinct `primary_tags_join`.`id`) + count(distinct `tags_join`.`tag_id`)) = 4)
|
|
|
|
length := len(tagsFilter.Value)
|
|
|
|
if tagsFilter.Modifier == models.CriterionModifierIncludes || tagsFilter.Modifier == models.CriterionModifierIncludesAll {
|
|
body += " LEFT JOIN tags AS ptj ON ptj.id = scene_markers.primary_tag_id AND ptj.id IN " + getInBinding(length)
|
|
body += " LEFT JOIN scene_markers_tags AS tj ON tj.scene_marker_id = scene_markers.id AND tj.tag_id IN " + getInBinding(length)
|
|
|
|
// only one required for include any
|
|
requiredCount := 1
|
|
|
|
// all required for include all
|
|
if tagsFilter.Modifier == models.CriterionModifierIncludesAll {
|
|
requiredCount = length
|
|
}
|
|
|
|
havingClauses = append(havingClauses, "((COUNT(DISTINCT ptj.id) + COUNT(DISTINCT tj.tag_id)) >= "+strconv.Itoa(requiredCount)+")")
|
|
} else if tagsFilter.Modifier == models.CriterionModifierExcludes {
|
|
// excludes all of the provided ids
|
|
whereClauses = append(whereClauses, "scene_markers.primary_tag_id not in "+getInBinding(length))
|
|
whereClauses = append(whereClauses, "not exists (select smt.scene_marker_id from scene_markers_tags as smt where smt.scene_marker_id = scene_markers.id and smt.tag_id in "+getInBinding(length)+")")
|
|
}
|
|
|
|
for _, tagID := range tagsFilter.Value {
|
|
args = append(args, tagID)
|
|
}
|
|
for _, tagID := range tagsFilter.Value {
|
|
args = append(args, tagID)
|
|
}
|
|
}
|
|
|
|
if sceneTagsFilter := sceneMarkerFilter.SceneTags; sceneTagsFilter != nil && len(sceneTagsFilter.Value) > 0 {
|
|
length := len(sceneTagsFilter.Value)
|
|
|
|
if sceneTagsFilter.Modifier == models.CriterionModifierIncludes || sceneTagsFilter.Modifier == models.CriterionModifierIncludesAll {
|
|
body += " LEFT JOIN scenes_tags AS scene_tags_join ON scene_tags_join.scene_id = scene.id AND scene_tags_join.tag_id IN " + getInBinding(length)
|
|
|
|
// only one required for include any
|
|
requiredCount := 1
|
|
|
|
// all required for include all
|
|
if sceneTagsFilter.Modifier == models.CriterionModifierIncludesAll {
|
|
requiredCount = length
|
|
}
|
|
|
|
havingClauses = append(havingClauses, "COUNT(DISTINCT scene_tags_join.tag_id) >= "+strconv.Itoa(requiredCount))
|
|
} else if sceneTagsFilter.Modifier == models.CriterionModifierExcludes {
|
|
// excludes all of the provided ids
|
|
whereClauses = append(whereClauses, "not exists (select st.scene_id from scenes_tags as st where st.scene_id = scene.id AND st.tag_id IN "+getInBinding(length)+")")
|
|
}
|
|
|
|
for _, tagID := range sceneTagsFilter.Value {
|
|
args = append(args, tagID)
|
|
}
|
|
}
|
|
|
|
if performersFilter := sceneMarkerFilter.Performers; performersFilter != nil && len(performersFilter.Value) > 0 {
|
|
length := len(performersFilter.Value)
|
|
|
|
if performersFilter.Modifier == models.CriterionModifierIncludes || performersFilter.Modifier == models.CriterionModifierIncludesAll {
|
|
body += " LEFT JOIN performers_scenes as scene_performers ON scene.id = scene_performers.scene_id"
|
|
whereClauses = append(whereClauses, "scene_performers.performer_id IN "+getInBinding(length))
|
|
|
|
// only one required for include any
|
|
requiredCount := 1
|
|
|
|
// all required for include all
|
|
if performersFilter.Modifier == models.CriterionModifierIncludesAll {
|
|
requiredCount = length
|
|
}
|
|
|
|
havingClauses = append(havingClauses, "COUNT(DISTINCT scene_performers.performer_id) >= "+strconv.Itoa(requiredCount))
|
|
} else if performersFilter.Modifier == models.CriterionModifierExcludes {
|
|
// excludes all of the provided ids
|
|
whereClauses = append(whereClauses, "not exists (select sp.scene_id from performers_scenes as sp where sp.scene_id = scene.id AND sp.performer_id IN "+getInBinding(length)+")")
|
|
}
|
|
|
|
for _, performerID := range performersFilter.Value {
|
|
args = append(args, performerID)
|
|
}
|
|
}
|
|
|
|
if q := findFilter.Q; q != nil && *q != "" {
|
|
searchColumns := []string{"scene_markers.title", "scene.title"}
|
|
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
|
|
whereClauses = append(whereClauses, clause)
|
|
args = append(args, thisArgs...)
|
|
}
|
|
|
|
if tagID := sceneMarkerFilter.TagID; tagID != nil {
|
|
whereClauses = append(whereClauses, "(scene_markers.primary_tag_id = "+*tagID+" OR tags.id = "+*tagID+")")
|
|
}
|
|
|
|
sortAndPagination := qb.getSceneMarkerSort(findFilter) + getPagination(findFilter)
|
|
idsResult, countResult, err := qb.executeFindQuery(body, args, sortAndPagination, whereClauses, havingClauses, []string{})
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
var sceneMarkers []*models.SceneMarker
|
|
for _, id := range idsResult {
|
|
sceneMarker, err := qb.Find(id)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
sceneMarkers = append(sceneMarkers, sceneMarker)
|
|
}
|
|
|
|
return sceneMarkers, countResult, nil
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) getSceneMarkerSort(findFilter *models.FindFilterType) string {
|
|
sort := findFilter.GetSort("title")
|
|
direction := findFilter.GetDirection()
|
|
tableName := "scene_markers"
|
|
if sort == "scenes_updated_at" {
|
|
sort = "updated_at"
|
|
tableName = "scene"
|
|
}
|
|
return getSort(sort, direction, tableName)
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) querySceneMarkers(query string, args []interface{}) ([]*models.SceneMarker, error) {
|
|
var ret models.SceneMarkers
|
|
if err := qb.query(query, args, &ret); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return []*models.SceneMarker(ret), nil
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) queryMarkerStringsResultType(query string, args []interface{}) ([]*models.MarkerStringsResultType, error) {
|
|
rows, err := database.DB.Queryx(query, args...)
|
|
if err != nil && err != sql.ErrNoRows {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
markerStrings := make([]*models.MarkerStringsResultType, 0)
|
|
for rows.Next() {
|
|
markerString := models.MarkerStringsResultType{}
|
|
if err := rows.StructScan(&markerString); err != nil {
|
|
return nil, err
|
|
}
|
|
markerStrings = append(markerStrings, &markerString)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return markerStrings, nil
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) tagsRepository() *joinRepository {
|
|
return &joinRepository{
|
|
repository: repository{
|
|
tx: qb.tx,
|
|
tableName: "scene_markers_tags",
|
|
idColumn: "scene_marker_id",
|
|
},
|
|
fkColumn: tagIDColumn,
|
|
}
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) GetTagIDs(id int) ([]int, error) {
|
|
return qb.tagsRepository().getIDs(id)
|
|
}
|
|
|
|
func (qb *sceneMarkerQueryBuilder) UpdateTags(id int, tagIDs []int) error {
|
|
// Delete the existing joins and then create new ones
|
|
return qb.tagsRepository().replace(id, tagIDs)
|
|
}
|