feat(predicates): added runtime predicate

This commit is contained in:
psyko-gh
2024-08-24 11:05:45 +02:00
parent 8133550bd3
commit b026ffad84
7 changed files with 81 additions and 3 deletions

View File

@@ -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`

View File

@@ -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
}

View File

@@ -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);
});
});

View File

@@ -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;
};

View File

@@ -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) {

View File

@@ -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),
};

View File

@@ -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<string, number> = {
minutes: 60,
hours: 60 * 60,
days: 24 * 60 * 60,
weeks: 7 * 24 * 60 * 60,