Add query options for tags, performers, studios #29 (#157)

* Add query options for tags, performers, studios

* Remove errant log

* Apply expanded query criteria to scene markers
This commit is contained in:
WithoutPants
2019-10-28 00:05:54 +11:00
committed by Leopere
parent 0655223c38
commit d0730c7243
11 changed files with 178 additions and 57 deletions

View File

@@ -28,11 +28,11 @@ input SceneMarkerFilterType {
"""Filter to only include scene markers with this tag""" """Filter to only include scene markers with this tag"""
tag_id: ID tag_id: ID
"""Filter to only include scene markers with these tags""" """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""" """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""" """Filter to only include scene markers with these performers"""
performers: [ID!] performers: MultiCriterionInput
} }
input SceneFilterType { input SceneFilterType {
@@ -45,11 +45,11 @@ input SceneFilterType {
"""Filter to only include scenes missing this property""" """Filter to only include scenes missing this property"""
is_missing: String is_missing: String
"""Filter to only include scenes with this studio""" """Filter to only include scenes with this studio"""
studio_id: ID studios: MultiCriterionInput
"""Filter to only include scenes with these tags""" """Filter to only include scenes with these tags"""
tags: [ID!] tags: MultiCriterionInput
"""Filter to only include scenes with this performer""" """Filter to only include scenes with these performers"""
performer_id: ID performers: MultiCriterionInput
} }
enum CriterionModifier { enum CriterionModifier {
@@ -65,6 +65,8 @@ enum CriterionModifier {
IS_NULL, IS_NULL,
"""IS NOT NULL""" """IS NOT NULL"""
NOT_NULL, NOT_NULL,
"""INCLUDES ALL"""
INCLUDES_ALL,
INCLUDES, INCLUDES,
EXCLUDES, EXCLUDES,
} }
@@ -73,3 +75,8 @@ input IntCriterionInput {
value: Int! value: Int!
modifier: CriterionModifier! modifier: CriterionModifier!
} }
input MultiCriterionInput {
value: [ID!]
modifier: CriterionModifier!
}

View File

@@ -218,23 +218,34 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin
} }
} }
if tagsFilter := sceneFilter.Tags; len(tagsFilter) > 0 { if tagsFilter := sceneFilter.Tags; tagsFilter != nil && len(tagsFilter.Value) > 0 {
for _, tagID := range tagsFilter { for _, tagID := range tagsFilter.Value {
args = append(args, tagID) args = append(args, tagID)
} }
whereClauses = append(whereClauses, "tags.id IN "+getInBinding(len(tagsFilter))) whereClause, havingClause := getMultiCriterionClause("tags", "scenes_tags", "tag_id", tagsFilter)
havingClauses = append(havingClauses, "count(distinct tags.id) IS "+strconv.Itoa(len(tagsFilter))) whereClauses = appendClause(whereClauses, whereClause)
havingClauses = appendClause(havingClauses, havingClause)
} }
if performerID := sceneFilter.PerformerID; performerID != nil { if performersFilter := sceneFilter.Performers; performersFilter != nil && len(performersFilter.Value) > 0 {
whereClauses = append(whereClauses, "performers.id = ?") for _, performerID := range performersFilter.Value {
args = append(args, *performerID) args = append(args, performerID)
} }
if studioID := sceneFilter.StudioID; studioID != nil { whereClause, havingClause := getMultiCriterionClause("performers", "performers_scenes", "performer_id", performersFilter)
whereClauses = append(whereClauses, "studio.id = ?") whereClauses = appendClause(whereClauses, whereClause)
args = append(args, *studioID) havingClauses = appendClause(havingClauses, havingClause)
}
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) sortAndPagination := qb.getSceneSort(findFilter) + getPagination(findFilter)
@@ -249,6 +260,37 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin
return scenes, countResult 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 { func (qb *SceneQueryBuilder) getSceneSort(findFilter *FindFilterType) string {
if findFilter == nil { if findFilter == nil {
return " ORDER BY scenes.path, scenes.date ASC " return " ORDER BY scenes.path, scenes.date ASC "

View File

@@ -134,7 +134,7 @@ func (qb *SceneMarkerQueryBuilder) Query(sceneMarkerFilter *SceneMarkerFilterTyp
left join tags on tags_join.tag_id = tags.id 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` //select `scene_markers`.* from `scene_markers`
//left join `tags` as `primary_tags_join` //left join `tags` as `primary_tags_join`
// on `primary_tags_join`.`id` = `scene_markers`.`primary_tag_id` // 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` //group by `scene_markers`.`id`
//having ((count(distinct `primary_tags_join`.`id`) + count(distinct `tags_join`.`tag_id`)) = 4) //having ((count(distinct `primary_tags_join`.`id`) + count(distinct `tags_join`.`tag_id`)) = 4)
length := len(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 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) 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 { // 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) args = append(args, tagID)
} }
for _, tagID := range tagIDs { for _, tagID := range tagsFilter.Value {
args = append(args, tagID) args = append(args, tagID)
} }
} }
if sceneTagIDs := sceneMarkerFilter.SceneTags; sceneTagIDs != nil { if sceneTagsFilter := sceneMarkerFilter.SceneTags; sceneTagsFilter != nil && len(sceneTagsFilter.Value) > 0 {
length := len(sceneTagIDs) 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) 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 { // 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) args = append(args, tagID)
} }
} }
if performerIDs := sceneMarkerFilter.Performers; performerIDs != nil { if performersFilter := sceneMarkerFilter.Performers; performersFilter != nil && len(performersFilter.Value) > 0 {
length := len(performerIDs) 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" 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)) whereClauses = append(whereClauses, "scene_performers.performer_id IN "+getInBinding(length))
for _, performerID := range performerIDs {
// 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) args = append(args, performerID)
} }
} }

View File

@@ -85,7 +85,7 @@ func getSort(sort string, direction string, tableName string) string {
if tableName == "scenes" { if tableName == "scenes" {
additional = ", bitrate DESC, framerate DESC, rating DESC, duration DESC" additional = ", bitrate DESC, framerate DESC, rating DESC, duration DESC"
} else if tableName == "scene_markers" { } 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 return " ORDER BY " + colName + " " + direction + additional
} }
@@ -204,9 +204,11 @@ func executeFindQuery(tableName string, body string, args []interface{}, sortAnd
idsResult, idsErr := runIdsQuery(idsQuery, args) idsResult, idsErr := runIdsQuery(idsQuery, args)
if countErr != nil { if countErr != nil {
logger.Errorf("Error executing count query with SQL: %s, args: %v, error: %s", countQuery, args, countErr.Error())
panic(countErr) panic(countErr)
} }
if idsErr != nil { if idsErr != nil {
logger.Errorf("Error executing find query with SQL: %s, args: %v, error: %s", idsQuery, args, idsErr.Error())
panic(idsErr) panic(idsErr)
} }

View File

@@ -53,9 +53,6 @@ export const Stats: FunctionComponent = () => {
<pre> <pre>
{` {`
This is still an early version, some things are still a work in progress. 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> </pre>
</div> </div>

View File

@@ -130,10 +130,14 @@ export const AddFilter: FunctionComponent<IAddFilterProps> = (props: IAddFilterP
} }
} }
return ( return (
<>
<FormGroup> <FormGroup>
{renderModifier()} {renderModifier()}
</FormGroup>
<FormGroup>
{renderSelect()} {renderSelect()}
</FormGroup> </FormGroup>
</>
); );
}; };

View File

@@ -38,6 +38,7 @@ export abstract class Criterion<Option = any, Value = any> {
case CriterionModifier.LessThan: return {value: CriterionModifier.LessThan, label: "Less Than"}; case CriterionModifier.LessThan: return {value: CriterionModifier.LessThan, label: "Less Than"};
case CriterionModifier.IsNull: return {value: CriterionModifier.IsNull, label: "Is NULL"}; case CriterionModifier.IsNull: return {value: CriterionModifier.IsNull, label: "Is NULL"};
case CriterionModifier.NotNull: return {value: CriterionModifier.NotNull, label: "Not 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.Includes: return {value: CriterionModifier.Includes, label: "Includes"};
case CriterionModifier.Excludes: return {value: CriterionModifier.Excludes, label: "Excludes"}; case CriterionModifier.Excludes: return {value: CriterionModifier.Excludes, label: "Excludes"};
} }
@@ -60,7 +61,8 @@ export abstract class Criterion<Option = any, Value = any> {
case CriterionModifier.IsNull: modifierString = "is null"; break; case CriterionModifier.IsNull: modifierString = "is null"; break;
case CriterionModifier.NotNull: modifierString = "is not null"; break; case CriterionModifier.NotNull: modifierString = "is not null"; break;
case CriterionModifier.Includes: modifierString = "includes"; 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 = ""; default: modifierString = "";
} }

View File

@@ -15,8 +15,12 @@ interface IOptionType {
export class PerformersCriterion extends Criterion<IOptionType, ILabeledId[]> { export class PerformersCriterion extends Criterion<IOptionType, ILabeledId[]> {
public type: CriterionType = "performers"; public type: CriterionType = "performers";
public parameterName: string = "performers"; public parameterName: string = "performers";
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.IncludesAll;
public modifierOptions = []; public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.IncludesAll),
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
public options: IOptionType[] = []; public options: IOptionType[] = [];
public value: ILabeledId[] = []; public value: ILabeledId[] = [];
} }

View File

@@ -15,8 +15,11 @@ interface IOptionType {
export class StudiosCriterion extends Criterion<IOptionType, ILabeledId[]> { export class StudiosCriterion extends Criterion<IOptionType, ILabeledId[]> {
public type: CriterionType = "studios"; public type: CriterionType = "studios";
public parameterName: string = "studios"; public parameterName: string = "studios";
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Includes;
public modifierOptions = []; public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
public options: IOptionType[] = []; public options: IOptionType[] = [];
public value: ILabeledId[] = []; public value: ILabeledId[] = [];
} }

View File

@@ -10,8 +10,12 @@ import {
export class TagsCriterion extends Criterion<GQL.AllTagsForFilterAllTags, ILabeledId[]> { export class TagsCriterion extends Criterion<GQL.AllTagsForFilterAllTags, ILabeledId[]> {
public type: CriterionType; public type: CriterionType;
public parameterName: string; public parameterName: string;
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.IncludesAll;
public modifierOptions = []; public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.IncludesAll),
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
public options: GQL.AllTagsForFilterAllTags[] = []; public options: GQL.AllTagsForFilterAllTags[] = [];
public value: ILabeledId[] = []; public value: ILabeledId[] = [];

View File

@@ -202,8 +202,8 @@ export class ListFilterModel {
this.criteria.forEach((criterion) => { this.criteria.forEach((criterion) => {
switch (criterion.type) { switch (criterion.type) {
case "rating": case "rating":
const crit = criterion as RatingCriterion; const ratingCrit = criterion as RatingCriterion;
result.rating = { value: crit.value, modifier: crit.modifier }; result.rating = { value: ratingCrit.value, modifier: ratingCrit.modifier };
break; break;
case "resolution": { case "resolution": {
switch ((criterion as ResolutionCriterion).value) { switch ((criterion as ResolutionCriterion).value) {
@@ -222,13 +222,16 @@ export class ListFilterModel {
result.is_missing = (criterion as IsMissingCriterion).value; result.is_missing = (criterion as IsMissingCriterion).value;
break; break;
case "tags": case "tags":
result.tags = (criterion as TagsCriterion).value.map((tag) => tag.id); const tagsCrit = criterion as TagsCriterion;
result.tags = { value: tagsCrit.value.map((tag) => tag.id), modifier: tagsCrit.modifier };
break; break;
case "performers": case "performers":
result.performer_id = (criterion as PerformersCriterion).value[0].id; // TODO: Allow multiple const perfCrit = criterion as PerformersCriterion;
result.performers = { value: perfCrit.value.map((perf) => perf.id), modifier: perfCrit.modifier };
break; break;
case "studios": case "studios":
result.studio_id = (criterion as StudiosCriterion).value[0].id; // TODO: Allow multiple const studCrit = criterion as StudiosCriterion;
result.studios = { value: studCrit.value.map((studio) => studio.id), modifier: studCrit.modifier };
break; break;
} }
}); });
@@ -252,13 +255,16 @@ export class ListFilterModel {
this.criteria.forEach((criterion) => { this.criteria.forEach((criterion) => {
switch (criterion.type) { switch (criterion.type) {
case "tags": case "tags":
result.tags = (criterion as TagsCriterion).value.map((tag) => tag.id); const tagsCrit = criterion as TagsCriterion;
result.tags = { value: tagsCrit.value.map((tag) => tag.id), modifier: tagsCrit.modifier };
break; break;
case "sceneTags": case "sceneTags":
result.scene_tags = (criterion as TagsCriterion).value.map((tag) => tag.id); const sceneTagsCrit = criterion as TagsCriterion;
result.scene_tags = { value: sceneTagsCrit.value.map((tag) => tag.id), modifier: sceneTagsCrit.modifier };
break; break;
case "performers": case "performers":
result.performers = (criterion as PerformersCriterion).value.map((performer) => performer.id); const performersCrit = criterion as PerformersCriterion;
result.performers = { value: performersCrit.value.map((performer) => performer.id), modifier: performersCrit.modifier };
break; break;
} }
}); });