diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index 600e871ee..7a7f356b5 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -64,6 +64,8 @@ input SceneFilterType { rating: IntCriterionInput """Filter by resolution""" resolution: ResolutionEnum + """Filter by duration (in seconds)""" + duration: IntCriterionInput """Filter to only include scenes which have markers. `true` or `false`""" has_markers: String """Filter to only include scenes missing this property""" diff --git a/pkg/models/querybuilder_scene.go b/pkg/models/querybuilder_scene.go index 7242937b5..5cbb988ce 100644 --- a/pkg/models/querybuilder_scene.go +++ b/pkg/models/querybuilder_scene.go @@ -171,13 +171,19 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin } if rating := sceneFilter.Rating; rating != nil { - clause, count := getIntCriterionWhereClause("rating", *sceneFilter.Rating) + clause, count := getIntCriterionWhereClause("scenes.rating", *sceneFilter.Rating) whereClauses = append(whereClauses, clause) if count == 1 { args = append(args, sceneFilter.Rating.Value) } } + if durationFilter := sceneFilter.Duration; durationFilter != nil { + clause, thisArgs := getDurationWhereClause(*durationFilter) + whereClauses = append(whereClauses, clause) + args = append(args, thisArgs...) + } + if resolutionFilter := sceneFilter.Resolution; resolutionFilter != nil { if resolution := resolutionFilter.String(); resolutionFilter.IsValid() { switch resolution { @@ -270,6 +276,34 @@ func appendClause(clauses []string, clause string) []string { return clauses } +func getDurationWhereClause(durationFilter IntCriterionInput) (string, []interface{}) { + // special case for duration. We accept duration as seconds as int but the + // field is floating point. Change the equals filter to return a range + // between x and x + 1 + // likewise, not equals needs to be duration < x OR duration >= x + var clause string + args := []interface{}{} + + value := durationFilter.Value + if durationFilter.Modifier == CriterionModifierEquals { + clause = "scenes.duration >= ? AND scenes.duration < ?" + args = append(args, value) + args = append(args, value+1) + } else if durationFilter.Modifier == CriterionModifierNotEquals { + clause = "(scenes.duration < ? OR scenes.duration >= ?)" + args = append(args, value) + args = append(args, value+1) + } else { + var count int + clause, count = getIntCriterionWhereClause("scenes.duration", durationFilter) + if count == 1 { + args = append(args, value) + } + } + + return clause, args +} + // returns where clause and having clause func getMultiCriterionClause(table string, joinTable string, joinTableField string, criterion *MultiCriterionInput) (string, string) { whereClause := "" diff --git a/ui/v2/src/components/Shared/DurationInput.tsx b/ui/v2/src/components/Shared/DurationInput.tsx index 6f9c15383..cdd32f9db 100644 --- a/ui/v2/src/components/Shared/DurationInput.tsx +++ b/ui/v2/src/components/Shared/DurationInput.tsx @@ -1,6 +1,6 @@ import React, { FunctionComponent, useState, useEffect } from "react"; import { InputGroup, ButtonGroup, Button, IInputGroupProps, HTMLInputProps, ControlGroup } from "@blueprintjs/core"; -import { TextUtils } from "../../utils/text"; +import { DurationUtils } from "../../utils/duration"; import { FIXED, NUMERIC_INPUT } from "@blueprintjs/core/lib/esm/common/classes"; interface IProps { @@ -11,65 +11,20 @@ interface IProps { } export const DurationInput: FunctionComponent = (props: IProps) => { - const [value, setValue] = useState(secondsToString(props.numericValue)); + const [value, setValue] = useState(DurationUtils.secondsToString(props.numericValue)); useEffect(() => { - setValue(secondsToString(props.numericValue)); + setValue(DurationUtils.secondsToString(props.numericValue)); }, [props.numericValue]); - function secondsToString(seconds : number) { - let ret = TextUtils.secondsToTimestamp(seconds); - - if (ret.startsWith("00:")) { - ret = ret.substr(3); - - if (ret.startsWith("0")) { - ret = ret.substr(1); - } - } - - return ret; - } - - function stringToSeconds(v : string) { - if (!v) { - return 0; - } - - let splits = v.split(":"); - - if (splits.length > 3) { - return 0; - } - - let seconds = 0; - let factor = 1; - while(splits.length > 0) { - let thisSplit = splits.pop(); - if (thisSplit == undefined) { - return 0; - } - - let thisInt = parseInt(thisSplit, 10); - if (isNaN(thisInt)) { - return 0; - } - - seconds += factor * thisInt; - factor *= 60; - } - - return seconds; - } - function increment() { - let seconds = stringToSeconds(value); + let seconds = DurationUtils.stringToSeconds(value); seconds += 1; props.onValueChange(seconds); } function decrement() { - let seconds = stringToSeconds(value); + let seconds = DurationUtils.stringToSeconds(value); seconds -= 1; props.onValueChange(seconds); } @@ -117,7 +72,7 @@ export const DurationInput: FunctionComponent = (props: disabled={props.disabled} value={value} onChange={(e : any) => setValue(e.target.value)} - onBlur={() => props.onValueChange(stringToSeconds(value))} + onBlur={() => props.onValueChange(DurationUtils.stringToSeconds(value))} placeholder="hh:mm:ss" rightElement={maybeRenderReset()} /> diff --git a/ui/v2/src/components/list/AddFilter.tsx b/ui/v2/src/components/list/AddFilter.tsx index fd3c9fa78..b672975ba 100644 --- a/ui/v2/src/components/list/AddFilter.tsx +++ b/ui/v2/src/components/list/AddFilter.tsx @@ -11,7 +11,7 @@ import _ from "lodash"; import React, { FunctionComponent, useEffect, useRef, useState } from "react"; import { isArray } from "util"; import { CriterionModifier } from "../../core/generated-graphql"; -import { Criterion, CriterionType } from "../../models/list-filter/criteria/criterion"; +import { Criterion, CriterionType, DurationCriterion } from "../../models/list-filter/criteria/criterion"; import { NoneCriterion } from "../../models/list-filter/criteria/none"; import { PerformersCriterion } from "../../models/list-filter/criteria/performers"; import { StudiosCriterion } from "../../models/list-filter/criteria/studios"; @@ -19,6 +19,7 @@ import { TagsCriterion } from "../../models/list-filter/criteria/tags"; import { makeCriteria } from "../../models/list-filter/criteria/utils"; import { ListFilterModel } from "../../models/list-filter/filter"; import { FilterMultiSelect } from "../select/FilterMultiSelect"; +import { DurationInput } from "../Shared/DurationInput"; interface IAddFilterProps { onAddCriterion: (criterion: Criterion, oldId?: string) => void; @@ -64,6 +65,11 @@ export const AddFilter: FunctionComponent = (props: IAddFilterP valueStage.current = event.target.value; } + function onChangedDuration(valueAsNumber: number) { + valueStage.current = valueAsNumber; + onBlurInput(); + } + function onBlurInput() { const newCriterion = _.cloneDeep(criterion); newCriterion.value = valueStage.current; @@ -148,6 +154,14 @@ export const AddFilter: FunctionComponent = (props: IAddFilterP defaultValue={criterion.value} /> ); + } else if (criterion instanceof DurationCriterion) { + // render duration control + return ( + + ) } else { return ( { case "none": return "None"; case "rating": return "Rating"; case "resolution": return "Resolution"; + case "duration": return "Duration"; case "favorite": return "Favorite"; case "hasMarkers": return "Has Markers"; case "isMissing": return "Is Missing"; @@ -91,10 +94,18 @@ export abstract class Criterion