From b026ffad844ca1fb83e7f20fdfbe1c109ee32b4a Mon Sep 17 00:00:00 2001 From: psyko-gh Date: Sat, 24 Aug 2024 11:05:45 +0200 Subject: [PATCH] feat(predicates): added runtime predicate --- README.md | 22 ++++++++++++++++++++++ schema/schema.json | 12 ++++++++++++ src/lib/rules/__tests__/rules.ts | 16 ++++++++++++++++ src/lib/rules/interfaces.ts | 5 +++++ src/lib/rules/predicate/adult.ts | 2 +- src/lib/rules/predicate/runtime.ts | 22 ++++++++++++++++++++++ src/lib/rules/predicate/time.ts | 5 +++-- 7 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 src/lib/rules/predicate/runtime.ts diff --git a/README.md b/README.md index 9e2de0e..66dd344 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,8 @@ Filters on the released status of the movie. Filters on the age of the movie. +See [Duration expressions](#duration-expressions) for more details + ```yaml - age: less than 2 years # or @@ -178,6 +180,19 @@ Filters on the score of the movie. - score: below 5.5 ``` +--- +### `runtime` + +Filters on the runtime _(duration)_ of the movie. + +See [Duration expressions](#duration-expressions) for more details + +```yaml + - runtime: less than 2.5 hours + # or + - runtime: more than 120 minutes +``` + --- ### `voteCount` @@ -316,3 +331,10 @@ Predicate that invert the result of its child predicate ``` --- + +### Duration expressions + +Duration expressions, like the one used in the `age` or `runtime` predicate can be expressed in the following way: +- an **operator**: `less than` or `more than` +- a integer or decimal **number**: `2` or `2.5` +- a **unit**: one of the following `year`, `month`, `week`, `day`, `hour`, `minute`. Singular or plural doesn't matter, so `hour` is the same as `hours` diff --git a/schema/schema.json b/schema/schema.json index c07e4b1..5da7463 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -104,6 +104,7 @@ { "$ref": "#/definitions/castPredicate" }, { "$ref": "#/definitions/crewPredicate" }, { "$ref": "#/definitions/releasedPredicate" }, + { "$ref": "#/definitions/runtimePredicate" }, { "$ref": "#/definitions/productionCompanyPredicate" } ] }, @@ -245,6 +246,17 @@ "properties": { "productionCompany": { "type": "array", "items": {"type": "string" }} } + }, + + "runtimePredicate": { + "$id": "#/definitions/runtimePredicate", + "type": "object", + "required": ["runtime"], + "properties": { + "runtime": { "type": "string" } + } } } + // End of definitions + } diff --git a/src/lib/rules/__tests__/rules.ts b/src/lib/rules/__tests__/rules.ts index a22978f..74e2901 100644 --- a/src/lib/rules/__tests__/rules.ts +++ b/src/lib/rules/__tests__/rules.ts @@ -11,6 +11,7 @@ import { AgePredicate } from '@core/lib/rules/predicate/age'; import * as movieJson from './movie.json'; import { MovieDetails } from '@core/api/overseerr/interfaces'; import { AdultPredicate } from '@core/lib/rules/predicate/adult'; +import { RuntimePredicate } from '@core/lib/rules/predicate/runtime'; const movie = movieJson as MovieDetails; const testRule = (predicate: Predicate | Predicate[]) => new Rule('test rule', Array.isArray(predicate) ? predicate : [predicate], 'accept'); @@ -257,3 +258,18 @@ describe('adult predicate', () => { assertRuleDoesntMatch(yesRule, movie); }); }); + +describe('runtime predicate', () => { + it('should match', async () => { + assertRuleMatches(testRule(new RuntimePredicate({ runtime: 'less than 3 hours' })), movie); + assertRuleMatches(testRule(new RuntimePredicate({ runtime: 'less than 2.5 hours' })), movie); + assertRuleMatches(testRule(new RuntimePredicate({ runtime: 'more than 2 hour' })), movie); + assertRuleMatches(testRule(new RuntimePredicate({ runtime: 'more than 2.25 hours' })), movie); + }); + + it('should not match', async () => { + assertRuleDoesntMatch(testRule(new RuntimePredicate({ runtime: 'more than 3 hours' })), movie); + assertRuleDoesntMatch(testRule(new RuntimePredicate({ runtime: 'more than 2.5 weeks' })), movie); + assertRuleDoesntMatch(testRule(new RuntimePredicate({ runtime: 'less than 2 minutes' })), movie); + }); +}); diff --git a/src/lib/rules/interfaces.ts b/src/lib/rules/interfaces.ts index 5e4cdc3..6a97870 100644 --- a/src/lib/rules/interfaces.ts +++ b/src/lib/rules/interfaces.ts @@ -24,6 +24,7 @@ export type PredicateOption = | KeywordOptions | ProductionCompanyOptions | ReleasedOptions + | RuntimeOptions | ScoreOptions | VoteCountOptions | WatchProvidersOptions; @@ -93,3 +94,7 @@ export type CrewJobNamesOptions = { job: string; names: string[]; }; + +export type RuntimeOptions = { + runtime: string; +}; diff --git a/src/lib/rules/predicate/adult.ts b/src/lib/rules/predicate/adult.ts index 095fde1..aac4bcc 100644 --- a/src/lib/rules/predicate/adult.ts +++ b/src/lib/rules/predicate/adult.ts @@ -1,7 +1,7 @@ import { PredicateBuilder } from '@core/lib/rules'; import { MovieDetails } from '@core/api/overseerr/interfaces'; import { BooleanPredicate } from '@core/lib/rules/predicate/boolean'; -import { AdultOptions, ReleasedOptions } from '@core/lib/rules/interfaces'; +import { AdultOptions } from '@core/lib/rules/interfaces'; export class AdultPredicate extends BooleanPredicate { constructor(options: AdultOptions) { diff --git a/src/lib/rules/predicate/runtime.ts b/src/lib/rules/predicate/runtime.ts new file mode 100644 index 0000000..58cc13e --- /dev/null +++ b/src/lib/rules/predicate/runtime.ts @@ -0,0 +1,22 @@ +import { MovieDetails } from '@core/api/overseerr/interfaces'; +import { PredicateBuilder } from '@core/lib/rules'; +import { fromHumanReadableDuration, TimePredicate } from '@core/lib/rules/predicate/time'; +import { RuntimeOptions } from '@core/lib/rules/interfaces'; + +export class RuntimePredicate extends TimePredicate { + constructor(options: RuntimeOptions) { + super(fromHumanReadableDuration(options.runtime), runtimeMetric); + } +} + +const runtimeMetric = (movie: MovieDetails): number => { + if (!movie.runtime) { + return 0; + } + return Number(movie.runtime) * 60; +}; + +export const RuntimePredicateBuilder: PredicateBuilder = { + key: 'runtime', + build: (data: RuntimeOptions) => new RuntimePredicate(data), +}; diff --git a/src/lib/rules/predicate/time.ts b/src/lib/rules/predicate/time.ts index 9526fb5..0555346 100644 --- a/src/lib/rules/predicate/time.ts +++ b/src/lib/rules/predicate/time.ts @@ -13,13 +13,14 @@ export class TimePredicate extends NumberPredicate { } } -export const fromHumanReadableDuration = (str: string): { threshold: number; operator: 'lt' | 'gt' } => { - const regex = /(less than|more than) (\d+(\.\d+)?) (years|year|days|day|month|months|weeks|week)/gm; +export const fromHumanReadableDuration = (str: string): NumberPredicateOptions => { + const regex = /(less than|more than) (\d+(\.\d+)?) (year|month|week|day|hour|minute)[s]?/gm; const groups = [...str.matchAll(regex)][0]; const operator = groups[1]; const threshold: number = Number(groups[2]); const units = groups[4].endsWith('s') ? groups[4] : `${groups[4]}s`; const scale: Record = { + minutes: 60, hours: 60 * 60, days: 24 * 60 * 60, weeks: 7 * 24 * 60 * 60,