Skip to content

Commit

Permalink
Add query options for tags, performers, studios #29 (#157)
Browse files Browse the repository at this point in the history
* Add query options for tags, performers, studios

* Remove errant log

* Apply expanded query criteria to scene markers
  • Loading branch information
WithoutPants authored and Leopere committed Oct 27, 2019
1 parent 0655223 commit d0730c7
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 57 deletions.
21 changes: 14 additions & 7 deletions graphql/schema/types/filters.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -65,11 +65,18 @@ enum CriterionModifier {
IS_NULL,
"""IS NOT NULL"""
NOT_NULL,
"""INCLUDES ALL"""
INCLUDES_ALL,
INCLUDES,
EXCLUDES,
}

input IntCriterionInput {
value: Int!
modifier: CriterionModifier!
}

input MultiCriterionInput {
value: [ID!]
modifier: CriterionModifier!
}
62 changes: 52 additions & 10 deletions pkg/models/querybuilder_scene.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 "
Expand Down
84 changes: 67 additions & 17 deletions pkg/models/querybuilder_scene_marker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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)
}
}
Expand Down
4 changes: 3 additions & 1 deletion pkg/models/querybuilder_sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}

Expand Down
3 changes: 0 additions & 3 deletions ui/v2/src/components/Stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,6 @@ export const Stats: FunctionComponent = () => {
<pre>
{`
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.
`}
</pre>
</div>
Expand Down
12 changes: 8 additions & 4 deletions ui/v2/src/components/list/AddFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,14 @@ export const AddFilter: FunctionComponent<IAddFilterProps> = (props: IAddFilterP
}
}
return (
<FormGroup>
{renderModifier()}
{renderSelect()}
</FormGroup>
<>
<FormGroup>
{renderModifier()}
</FormGroup>
<FormGroup>
{renderSelect()}
</FormGroup>
</>
);
};

Expand Down
4 changes: 3 additions & 1 deletion ui/v2/src/models/list-filter/criteria/criterion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export abstract class Criterion<Option = any, Value = any> {
case CriterionModifier.LessThan: return {value: CriterionModifier.LessThan, label: "Less Than"};
case CriterionModifier.IsNull: return {value: CriterionModifier.IsNull, label: "Is NULL"};
case CriterionModifier.NotNull: return {value: CriterionModifier.NotNull, label: "Not NULL"};
case CriterionModifier.IncludesAll: return {value: CriterionModifier.IncludesAll, label: "Includes All"};
case CriterionModifier.Includes: return {value: CriterionModifier.Includes, label: "Includes"};
case CriterionModifier.Excludes: return {value: CriterionModifier.Excludes, label: "Excludes"};
}
Expand All @@ -60,7 +61,8 @@ export abstract class Criterion<Option = any, Value = any> {
case CriterionModifier.IsNull: modifierString = "is null"; break;
case CriterionModifier.NotNull: modifierString = "is not null"; break;
case CriterionModifier.Includes: modifierString = "includes"; break;
case CriterionModifier.Excludes: modifierString = "exculdes"; break;
case CriterionModifier.IncludesAll: modifierString = "includes all"; break;
case CriterionModifier.Excludes: modifierString = "excludes"; break;
default: modifierString = "";
}

Expand Down
8 changes: 6 additions & 2 deletions ui/v2/src/models/list-filter/criteria/performers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ interface IOptionType {
export class PerformersCriterion extends Criterion<IOptionType, ILabeledId[]> {
public type: CriterionType = "performers";
public parameterName: string = "performers";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public modifier = CriterionModifier.IncludesAll;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.IncludesAll),
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
public options: IOptionType[] = [];
public value: ILabeledId[] = [];
}
Expand Down
7 changes: 5 additions & 2 deletions ui/v2/src/models/list-filter/criteria/studios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ interface IOptionType {
export class StudiosCriterion extends Criterion<IOptionType, ILabeledId[]> {
public type: CriterionType = "studios";
public parameterName: string = "studios";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public modifier = CriterionModifier.Includes;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
public options: IOptionType[] = [];
public value: ILabeledId[] = [];
}
Expand Down
8 changes: 6 additions & 2 deletions ui/v2/src/models/list-filter/criteria/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ import {
export class TagsCriterion extends Criterion<GQL.AllTagsForFilterAllTags, ILabeledId[]> {
public type: CriterionType;
public parameterName: string;
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public modifier = CriterionModifier.IncludesAll;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.IncludesAll),
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
public options: GQL.AllTagsForFilterAllTags[] = [];
public value: ILabeledId[] = [];

Expand Down
Loading

0 comments on commit d0730c7

Please sign in to comment.