diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index 0e3354208..58c364c47 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -28,11 +28,11 @@ input SceneMarkerFilterType { """Filter to only include scene markers with this tag""" tag_id: ID """Filter to only include scene markers with these tags""" - tags: [ID!] + tags: MultiCriterionInput """Filter to only include scene markers attached to a scene with these tags""" - scene_tags: [ID!] + scene_tags: MultiCriterionInput """Filter to only include scene markers with these performers""" - performers: [ID!] + performers: MultiCriterionInput } input SceneFilterType { @@ -45,11 +45,11 @@ input SceneFilterType { """Filter to only include scenes missing this property""" is_missing: String """Filter to only include scenes with this studio""" - studio_id: ID + studios: MultiCriterionInput """Filter to only include scenes with these tags""" - tags: [ID!] - """Filter to only include scenes with this performer""" - performer_id: ID + tags: MultiCriterionInput + """Filter to only include scenes with these performers""" + performers: MultiCriterionInput } enum CriterionModifier { @@ -65,6 +65,8 @@ enum CriterionModifier { IS_NULL, """IS NOT NULL""" NOT_NULL, + """INCLUDES ALL""" + INCLUDES_ALL, INCLUDES, EXCLUDES, } @@ -72,4 +74,9 @@ enum CriterionModifier { input IntCriterionInput { value: Int! modifier: CriterionModifier! +} + +input MultiCriterionInput { + value: [ID!] + modifier: CriterionModifier! } \ No newline at end of file diff --git a/pkg/models/querybuilder_scene.go b/pkg/models/querybuilder_scene.go index fc33aaab1..f19725d88 100644 --- a/pkg/models/querybuilder_scene.go +++ b/pkg/models/querybuilder_scene.go @@ -218,23 +218,34 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin } } - if tagsFilter := sceneFilter.Tags; len(tagsFilter) > 0 { - for _, tagID := range tagsFilter { + if tagsFilter := sceneFilter.Tags; tagsFilter != nil && len(tagsFilter.Value) > 0 { + for _, tagID := range tagsFilter.Value { args = append(args, tagID) } - whereClauses = append(whereClauses, "tags.id IN "+getInBinding(len(tagsFilter))) - havingClauses = append(havingClauses, "count(distinct tags.id) IS "+strconv.Itoa(len(tagsFilter))) + whereClause, havingClause := getMultiCriterionClause("tags", "scenes_tags", "tag_id", tagsFilter) + whereClauses = appendClause(whereClauses, whereClause) + havingClauses = appendClause(havingClauses, havingClause) } - if performerID := sceneFilter.PerformerID; performerID != nil { - whereClauses = append(whereClauses, "performers.id = ?") - args = append(args, *performerID) + if performersFilter := sceneFilter.Performers; performersFilter != nil && len(performersFilter.Value) > 0 { + for _, performerID := range performersFilter.Value { + args = append(args, performerID) + } + + whereClause, havingClause := getMultiCriterionClause("performers", "performers_scenes", "performer_id", performersFilter) + whereClauses = appendClause(whereClauses, whereClause) + havingClauses = appendClause(havingClauses, havingClause) } - if studioID := sceneFilter.StudioID; studioID != nil { - whereClauses = append(whereClauses, "studio.id = ?") - args = append(args, *studioID) + if studiosFilter := sceneFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 { + for _, studioID := range studiosFilter.Value { + args = append(args, studioID) + } + + whereClause, havingClause := getMultiCriterionClause("studio", "", "studio_id", studiosFilter) + whereClauses = appendClause(whereClauses, whereClause) + havingClauses = appendClause(havingClauses, havingClause) } sortAndPagination := qb.getSceneSort(findFilter) + getPagination(findFilter) @@ -249,6 +260,37 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin return scenes, countResult } +func appendClause(clauses []string, clause string) []string { + if clause != "" { + return append(clauses, clause) + } + + return clauses +} + +// returns where clause and having clause +func getMultiCriterionClause(table string, joinTable string, joinTableField string, criterion *MultiCriterionInput) (string, string) { + whereClause := "" + havingClause := "" + if criterion.Modifier == CriterionModifierIncludes { + // includes any of the provided ids + whereClause = table + ".id IN "+ getInBinding(len(criterion.Value)) + } else if criterion.Modifier == CriterionModifierIncludesAll { + // includes all of the provided ids + whereClause = table + ".id IN "+ getInBinding(len(criterion.Value)) + havingClause = "count(distinct " + table + ".id) IS " + strconv.Itoa(len(criterion.Value)) + } else if criterion.Modifier == CriterionModifierExcludes { + // excludes all of the provided ids + if (joinTable != "") { + whereClause = "not exists (select " + joinTable + ".scene_id from " + joinTable + " where " + joinTable + ".scene_id = scenes.id and " + joinTable + "." + joinTableField + " in " + getInBinding(len(criterion.Value)) + ")" + } else { + whereClause = "not exists (select s.id from scenes as s where s.id = scenes.id and s." + joinTableField + " in " + getInBinding(len(criterion.Value)) + ")" + } + } + + return whereClause, havingClause +} + func (qb *SceneQueryBuilder) getSceneSort(findFilter *FindFilterType) string { if findFilter == nil { return " ORDER BY scenes.path, scenes.date ASC " diff --git a/pkg/models/querybuilder_scene_marker.go b/pkg/models/querybuilder_scene_marker.go index a3530a704..1460d0dde 100644 --- a/pkg/models/querybuilder_scene_marker.go +++ b/pkg/models/querybuilder_scene_marker.go @@ -134,7 +134,7 @@ func (qb *SceneMarkerQueryBuilder) Query(sceneMarkerFilter *SceneMarkerFilterTyp left join tags on tags_join.tag_id = tags.id ` - if tagIDs := sceneMarkerFilter.Tags; tagIDs != nil { + 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` @@ -145,32 +145,82 @@ func (qb *SceneMarkerQueryBuilder) Query(sceneMarkerFilter *SceneMarkerFilterTyp //group by `scene_markers`.`id` //having ((count(distinct `primary_tags_join`.`id`) + count(distinct `tags_join`.`tag_id`)) = 4) - length := len(tagIDs) - 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) - havingClauses = append(havingClauses, "((COUNT(DISTINCT ptj.id) + COUNT(DISTINCT tj.tag_id)) = "+strconv.Itoa(length)+")") - for _, tagID := range tagIDs { + length := len(tagsFilter.Value) + + if tagsFilter.Modifier == CriterionModifierIncludes || tagsFilter.Modifier == 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 == CriterionModifierIncludesAll { + requiredCount = length + } + + havingClauses = append(havingClauses, "((COUNT(DISTINCT ptj.id) + COUNT(DISTINCT tj.tag_id)) >= "+strconv.Itoa(requiredCount)+")") + } else if tagsFilter.Modifier == 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 tagIDs { + for _, tagID := range tagsFilter.Value { args = append(args, tagID) } } - if sceneTagIDs := sceneMarkerFilter.SceneTags; sceneTagIDs != nil { - length := len(sceneTagIDs) - 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) - havingClauses = append(havingClauses, "COUNT(DISTINCT scene_tags_join.tag_id) = "+strconv.Itoa(length)) - for _, tagID := range sceneTagIDs { + if sceneTagsFilter := sceneMarkerFilter.SceneTags; sceneTagsFilter != nil && len(sceneTagsFilter.Value) > 0 { + length := len(sceneTagsFilter.Value) + + if sceneTagsFilter.Modifier == CriterionModifierIncludes || sceneTagsFilter.Modifier == 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 == CriterionModifierIncludesAll { + requiredCount = length + } + + havingClauses = append(havingClauses, "COUNT(DISTINCT scene_tags_join.tag_id) >= "+strconv.Itoa(requiredCount)) + } else if sceneTagsFilter.Modifier == 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 performerIDs := sceneMarkerFilter.Performers; performerIDs != nil { - length := len(performerIDs) - 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)) - for _, performerID := range performerIDs { + if performersFilter := sceneMarkerFilter.Performers; performersFilter != nil && len(performersFilter.Value) > 0 { + length := len(performersFilter.Value) + + if performersFilter.Modifier == CriterionModifierIncludes || performersFilter.Modifier == 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 == CriterionModifierIncludesAll { + requiredCount = length + } + + havingClauses = append(havingClauses, "COUNT(DISTINCT scene_performers.performer_id) >= "+strconv.Itoa(requiredCount)) + } else if performersFilter.Modifier == 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) } } diff --git a/pkg/models/querybuilder_sql.go b/pkg/models/querybuilder_sql.go index 7fe5c9363..59755837a 100644 --- a/pkg/models/querybuilder_sql.go +++ b/pkg/models/querybuilder_sql.go @@ -85,7 +85,7 @@ func getSort(sort string, direction string, tableName string) string { if tableName == "scenes" { additional = ", bitrate DESC, framerate DESC, rating DESC, duration DESC" } else if tableName == "scene_markers" { - additional = ", scene_id ASC, seconds ASC" + additional = ", scene_markers.scene_id ASC, scene_markers.seconds ASC" } return " ORDER BY " + colName + " " + direction + additional } @@ -204,9 +204,11 @@ func executeFindQuery(tableName string, body string, args []interface{}, sortAnd idsResult, idsErr := runIdsQuery(idsQuery, args) if countErr != nil { + logger.Errorf("Error executing count query with SQL: %s, args: %v, error: %s", countQuery, args, countErr.Error()) panic(countErr) } if idsErr != nil { + logger.Errorf("Error executing find query with SQL: %s, args: %v, error: %s", idsQuery, args, idsErr.Error()) panic(idsErr) } diff --git a/ui/v2/src/components/Stats.tsx b/ui/v2/src/components/Stats.tsx index 5ec380933..bca208438 100644 --- a/ui/v2/src/components/Stats.tsx +++ b/ui/v2/src/components/Stats.tsx @@ -53,9 +53,6 @@ export const Stats: FunctionComponent = () => {
{`
This is still an early version, some things are still a work in progress.
-
- * Filters for performers and studios only supports one item, even though it's a multi select.
-
`}
diff --git a/ui/v2/src/components/list/AddFilter.tsx b/ui/v2/src/components/list/AddFilter.tsx
index 2c3bec379..17661a547 100644
--- a/ui/v2/src/components/list/AddFilter.tsx
+++ b/ui/v2/src/components/list/AddFilter.tsx
@@ -130,10 +130,14 @@ export const AddFilter: FunctionComponent