Merge pull request #3 from psyko-gh/feature-bases

Feature bases
This commit is contained in:
psyko-gh
2024-08-24 17:12:15 +02:00
committed by GitHub
10 changed files with 474 additions and 225 deletions

209
README.md
View File

@@ -9,7 +9,7 @@ CRON based bot that automatically requests movies you may like in Overseer using
rules:
- name: Acceptable sci-fi movie
whenMatch:
- genres:
- genre:
- science-fiction
- score: above 5
action: accept
@@ -55,6 +55,7 @@ docker run -d \
-v /path/to/config:/config \
ghcr.io/psyko-gh/overcrawlrr:latest
```
# Configuration
## Configure Overseerr
@@ -145,15 +146,14 @@ When matching:
## Rule predicates
---
### `released`
### `adult`
Filters on the released status of the movie.
Filters on the adult status of the movie.
```yaml
- released: yes
- adult: yes
# or
- released: no
- adult: no
```
---
@@ -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
@@ -168,56 +170,16 @@ Filters on the age of the movie.
```
---
### `score`
### `and`
Filters on the score of the movie.
Predicate that will match if all of its predicate matches
```yaml
- score: above 6.5
# or
- score: below 5.5
```
---
### `voteCount`
Filters on the vote count of the movie.
```yaml
- voteCount: above 1000
# or
- voteCount: below 100
```
---
### `genre`
Filters on the genre of the movie. Will match when one or more of the listed genres matches the genre of the movie.
**Case insensitive**
```yaml
- genres:
- animation
- romance
```
---
### `watchProviders`
Filters based on the available Streaming/VOD platforms. Will match when one or more of the listed provider matches.
**Case insensitive**
```yaml
# This predicate will match when the movie is available in Germany on Netflix or Amazon Prime
- watchProviders:
# ISO 3166-1 alpha-2 format of the region (de, au, us, fr...)
- region: de
- names:
- Netflix
- Amazon Prime
# Will match if the movie is less than 2 years old AND if the movie genre is 'animation'
- and:
- age: less than 2 years
- genre:
- animation
```
---
@@ -254,18 +216,47 @@ It is also possible to specify the job
- James Cameron
- Steven Spielberg
```
---
### `productionCompany`
Filters based on the production companies of the movie. Will match when one or more of the listed company matches.
---
### `genre`
Filters on the genre of the movie. Will match when one or more of the listed genres matches the genre of the movie.
**Case insensitive**
```yaml
- productionCompany:
- 20th Century Fox
- Warner Bros. Pictures
- Twisted Pictures
- genre: musical
# or with an array of values
- genre:
- animation
- romance
```
---
### `not`
Predicate that invert the result of its child predicate
```yaml
- not:
- genre:
- animation
```
---
### `originalLanguage`
Filters on the original language of the movie
**Case insensitive**
```yaml
# ISO 639-1 format of the language (de, au, us, fr...)
- originalLanguage: en
# or with an array of values
- originalLanguage:
- en
- fr
```
---
@@ -281,33 +272,107 @@ Predicate that will match if any of its predicate matches
```
---
### `and`
### `productionCompany`
Predicate that will match if all of its predicate matches
Filters based on the production companies of the movie. Will match when one or more of the listed company matches.
**Case insensitive**
```yaml
# Will match if the movie is less than 2 years old AND if the movie genre is 'animation'
- and:
- age: less than 2 years
- genres:
- animation
- productionCompany:
- 20th Century Fox
- Warner Bros. Pictures
- Twisted Pictures
```
---
### `not`
### `released`
Predicate that invert the result of its child predicate
Filters on the released status of the movie.
```yaml
- not:
- genres:
- animation
- released: yes
# or
- released: no
```
---
### `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
```
---
### `score`
Filters on the score of the movie.
```yaml
- score: above 6.5
# or
- score: below 5.5
```
---
### `status`
Filters on the status of the movie.
The possible values are the one provided by [TMDB](https://www.themoviedb.org/): `rumored`, `planned`, `in production`, `post production`, `released`, `canceled`
**Case insensitive**
```yaml
- status: released
# or with an array of values
- status:
- released
- post production
- planned
```
---
### `voteCount`
Filters on the vote count of the movie.
```yaml
- voteCount: above 1000
# or
- voteCount: below 100
```
---
### `watchProviders`
Filters based on the available Streaming/VOD platforms. Will match when one or more of the listed provider matches.
**Case insensitive**
```yaml
# This predicate will match when the movie is available in Germany on Netflix or Amazon Prime
- watchProviders:
# ISO 3166-1 alpha-2 format of the region (de, au, us, fr...)
- region: de
- names:
- Netflix
- Amazon Prime
```
---
### 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

@@ -95,6 +95,7 @@
{ "$ref": "#/definitions/orPredicate" },
{ "$ref": "#/definitions/andPredicate" },
{ "$ref": "#/definitions/notPredicate" },
{ "$ref": "#/definitions/adultPredicate" },
{ "$ref": "#/definitions/agePredicate" },
{ "$ref": "#/definitions/scorePredicate" },
{ "$ref": "#/definitions/genrePredicate" },
@@ -103,7 +104,10 @@
{ "$ref": "#/definitions/castPredicate" },
{ "$ref": "#/definitions/crewPredicate" },
{ "$ref": "#/definitions/releasedPredicate" },
{ "$ref": "#/definitions/productionCompanyPredicate" }
{ "$ref": "#/definitions/runtimePredicate" },
{ "$ref": "#/definitions/productionCompanyPredicate" },
{ "$ref": "#/definitions/originalLanguagePredicate" },
{ "$ref": "#/definitions/statusPredicate" },
]
},
@@ -134,6 +138,15 @@
}
},
"adultPredicate": {
"$id": "#/definitions/adultPredicate",
"type": "object",
"required": ["adult"],
"properties": {
"adult": { "type": "string", "enum": ["yes", "no"]}
}
},
"agePredicate": {
"$id": "#/definitions/agePredicate",
"type": "object",
@@ -235,6 +248,43 @@
"properties": {
"productionCompany": { "type": "array", "items": {"type": "string" }}
}
},
"runtimePredicate": {
"$id": "#/definitions/runtimePredicate",
"type": "object",
"required": ["runtime"],
"properties": {
"runtime": { "type": "string" }
}
},
"originalLanguagePredicate": {
"$id": "#/definitions/originalLanguagePredicate",
"type": "object",
"required": ["originalLanguage"],
"properties": {
"originalLanguage": {
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" }}
]
}
}
},
"statusPredicate": {
"$id": "#/definitions/statusPredicate",
"type": "object",
"required": ["status"],
"properties": {
"status": {
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" }}
]
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
import { Rule } from '@core/lib/rules';
import { Predicate, Rule } from '@core/lib/rules';
import { FalsePredicate, TruePredicate } from '@core/lib/rules/predicate';
import { GenrePredicate } from '@core/lib/rules/predicate/genre';
import { CastPredicate } from '@core/lib/rules/predicate/cast';
@@ -10,108 +10,107 @@ import { VoteCountPredicate } from '@core/lib/rules/predicate/voteCount';
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';
import { OriginalLanguagePredicate } from '@core/lib/rules/predicate/originalLanguage';
import { StatusPredicate } from '@core/lib/rules/predicate/status';
const movie = movieJson as MovieDetails;
const testRule = (predicate: Predicate | Predicate[]) => new Rule('test rule', Array.isArray(predicate) ? predicate : [predicate], 'accept');
const assertRuleResult = (rule: Rule, matchExpected: boolean, testMovie: MovieDetails = movie) => {
const evaluation = rule.matches(testMovie);
expect(evaluation).toEqual(matchExpected);
};
const assertRuleMatches = (rule: Rule, testMovie: MovieDetails = movie) => {
assertRuleResult(rule, true, testMovie);
};
const assertRuleDoesntMatch = (rule: Rule, testMovie: MovieDetails = movie) => {
assertRuleResult(rule, false, testMovie);
};
describe('truePredicate', () => {
const rule = new Rule('simple', [new TruePredicate()], 'accept');
const rule = testRule(new TruePredicate());
it('should match', async () => {
const response = rule.matches(movie);
expect(response).toEqual(true);
assertRuleMatches(rule, movie);
});
});
describe('falsePredicate', () => {
const rule = new Rule('simple', [new FalsePredicate()], 'accept');
const rule = testRule(new FalsePredicate());
it('should not match', async () => {
const response = rule.matches(movie);
expect(response).toEqual(false);
});
it('should not match', async () => assertRuleDoesntMatch(rule, movie));
});
describe('defaultPredicate', () => {
it('should match when all children match', async () => {
const rule = new Rule('simple', [new TruePredicate(), new TruePredicate()], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(true);
const rule = testRule([new TruePredicate(), new TruePredicate()]);
assertRuleMatches(rule, movie);
});
it('should not match when any child does not', async () => {
const rule = new Rule('simple', [new TruePredicate(), new FalsePredicate()], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(false);
const rule = testRule([new TruePredicate(), new FalsePredicate()]);
assertRuleDoesntMatch(rule, movie);
});
});
describe('andPredicate', () => {
it('should match when all children match', async () => {
const rule = new Rule('simple', [new AndPredicate([new TruePredicate(), new TruePredicate()])], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(true);
const rule = testRule([new AndPredicate([new TruePredicate(), new TruePredicate()])]);
assertRuleMatches(rule, movie);
});
it('should not match when any child does not', async () => {
const rule = new Rule('simple', [new AndPredicate([new TruePredicate(), new FalsePredicate()])], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(false);
const rule = testRule([new AndPredicate([new TruePredicate(), new FalsePredicate()])]);
assertRuleDoesntMatch(rule, movie);
});
});
describe('orPredicate', () => {
it('should match when all children match', async () => {
const rule = new Rule('simple', [new OrPredicate([new TruePredicate(), new TruePredicate()])], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(true);
const rule = testRule([new OrPredicate([new TruePredicate(), new TruePredicate()])]);
assertRuleMatches(rule, movie);
});
it('should match when at least one child matches', async () => {
const rule = new Rule('simple', [new OrPredicate([new TruePredicate(), new FalsePredicate()])], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(true);
const rule = testRule([new OrPredicate([new TruePredicate(), new FalsePredicate()])]);
assertRuleMatches(rule, movie);
});
it('should not match when no child matches', async () => {
const rule = new Rule('simple', [new OrPredicate([new FalsePredicate(), new FalsePredicate()])], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(false);
const rule = testRule([new OrPredicate([new FalsePredicate(), new FalsePredicate()])]);
assertRuleDoesntMatch(rule, movie);
});
it('should not match when vote count below 8554 or above 8556', async () => {
{
const rule = new Rule('simple', [new VoteCountPredicate({ voteCount: 'less than 8554' })], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(false);
const rule = testRule(new VoteCountPredicate({ voteCount: 'less than 8554' }));
assertRuleDoesntMatch(rule, movie);
}
{
const rule = new Rule('simple', [new VoteCountPredicate({ voteCount: 'above 8556' })], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(false);
const rule = testRule(new VoteCountPredicate({ voteCount: 'above 8556' }));
assertRuleDoesntMatch(rule, movie);
}
});
it('should match when vote count above 8555', async () => {
{
const rule = new Rule('simple', [new VoteCountPredicate({ voteCount: 'above 8554' })], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(true);
const rule = testRule(new VoteCountPredicate({ voteCount: 'above 8554' }));
assertRuleMatches(rule, movie);
}
{
const rule = new Rule('simple', [new VoteCountPredicate({ voteCount: 'less than 8556' })], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(true);
const rule = testRule(new VoteCountPredicate({ voteCount: 'less than 8556' }));
assertRuleMatches(rule, movie);
}
});
it('should match genres', async () => {
{
const rule = new Rule('simple', [new GenrePredicate({ genre: 'science-fiction' })], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(true);
const rule = testRule(new GenrePredicate({ genre: 'science-fiction' }));
assertRuleMatches(rule, movie);
}
{
const rule = new Rule(
@@ -123,167 +122,180 @@ describe('orPredicate', () => {
],
'accept'
);
const response = rule.matches(movie);
expect(response).toEqual(true);
assertRuleMatches(rule, movie);
}
{
const rule = new Rule('simple', [new GenrePredicate({ genre: 'comédie' })], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(false);
const rule = testRule(new GenrePredicate({ genre: 'comédie' }));
assertRuleDoesntMatch(rule, movie);
}
});
});
describe('agePredicate', () => {
it('should match', async () => {
const rule = new Rule('simple', [new AgePredicate({ age: 'more than 1 year' })], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(true);
const rule = testRule(new AgePredicate({ age: 'more than 1 year' }));
assertRuleMatches(rule, movie);
});
});
describe('castPredicate', () => {
it('should match', async () => {
const rule = new Rule('simple', [new CastPredicate({ cast: ['Sigourney Weaver'] })], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(true);
const rule = testRule(new CastPredicate({ cast: ['Sigourney Weaver'] }));
assertRuleMatches(rule, movie);
});
it('should match despite different case', async () => {
const rule = new Rule('simple', [new CastPredicate({ cast: ['SiGoUrnEy weAVEr'] })], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(true);
const rule = testRule(new CastPredicate({ cast: ['SiGoUrnEy weAVEr'] }));
assertRuleMatches(rule, movie);
});
it('should match if at least one cast match', async () => {
const rule = new Rule('simple', [new CastPredicate({ cast: ['Sigourney Weaver', 'Jessica Alba'] })], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(true);
const rule = testRule([new CastPredicate({ cast: ['Sigourney Weaver', 'Jessica Alba'] })]);
assertRuleMatches(rule, movie);
});
it('should not match', async () => {
const rule = new Rule('simple', [new CastPredicate({ cast: ['Jessica Alba'] })], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(false);
const rule = testRule(new CastPredicate({ cast: ['Jessica Alba'] }));
assertRuleDoesntMatch(rule, movie);
});
});
describe('crewPredicate', () => {
it('should match', async () => {
const rule = new Rule('simple', [new CrewPredicate({ crew: ['James Cameron'] })], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(true);
const rule = testRule(new CrewPredicate({ crew: ['James Cameron'] }));
assertRuleMatches(rule, movie);
});
it('should match with a job', async () => {
const rule = new Rule(
'simple',
[
new CrewPredicate({
crew: { job: 'director', names: ['James Cameron'] },
}),
],
'accept'
const rule = testRule(
new CrewPredicate({
crew: { job: 'director', names: ['James Cameron'] },
})
);
const response = rule.matches(movie);
expect(response).toEqual(true);
assertRuleMatches(rule, movie);
});
it('should match despite different case', async () => {
const rule = new Rule('simple', [new CrewPredicate({ crew: ['JaMEs CamerOn'] })], 'accept');
const rule = testRule(new CrewPredicate({ crew: ['JaMEs CamerOn'] }));
const response = rule.matches(movie);
expect(response).toEqual(true);
assertRuleMatches(rule, movie);
});
it('should match if at least one crew match', async () => {
const rule = new Rule(
'simple',
[
new CrewPredicate({
crew: {
job: 'Art Direction',
names: ['Bert Davey', 'Ken Court'],
},
}),
],
'accept'
const rule = testRule(
new CrewPredicate({
crew: {
job: 'Art Direction',
names: ['Bert Davey', 'Ken Court'],
},
})
);
const response = rule.matches(movie);
expect(response).toEqual(true);
assertRuleMatches(rule, movie);
});
it('should not match', async () => {
const rule = new Rule('simple', [new CrewPredicate({ crew: ['Jessica Alba'] })], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(false);
const rule = testRule(new CrewPredicate({ crew: ['Jessica Alba'] }));
assertRuleDoesntMatch(rule, movie);
});
it('should not match when person found in another job', async () => {
const rule = new Rule(
'simple',
[
new CrewPredicate({
crew: { job: 'director', names: ['Peter Lamont'] },
}),
],
'accept'
const rule = testRule(
new CrewPredicate({
crew: { job: 'director', names: ['Peter Lamont'] },
})
);
const response = rule.matches(movie);
expect(response).toEqual(false);
assertRuleDoesntMatch(rule, movie);
});
});
describe('keywordPredicate', () => {
it('should match string', async () => {
const rule = new Rule('simple', [new KeywordPredicate({ keyword: 'space travel' })], 'accept');
const rule = testRule(new KeywordPredicate({ keyword: 'space travel' }));
const response = rule.matches(movie);
expect(response).toEqual(true);
assertRuleMatches(rule, movie);
});
it('should match array', async () => {
const rule = new Rule('simple', [new KeywordPredicate({ keyword: ['space travel'] })], 'accept');
const rule = testRule(new KeywordPredicate({ keyword: ['space travel'] }));
const response = rule.matches(movie);
expect(response).toEqual(true);
assertRuleMatches(rule, movie);
});
it('should match despite different case', async () => {
const rule = new Rule('simple', [new KeywordPredicate({ keyword: ['SpAcE TraVel'] })], 'accept');
const rule = testRule(new KeywordPredicate({ keyword: ['SpAcE TraVel'] }));
const response = rule.matches(movie);
expect(response).toEqual(true);
assertRuleMatches(rule, movie);
});
it('should match if at least one cast match', async () => {
const rule = new Rule(
'simple',
[
new KeywordPredicate({
keyword: ['space travel', 'unknown keyword'],
}),
],
'accept'
const rule = testRule(
new KeywordPredicate({
keyword: ['space travel', 'unknown keyword'],
})
);
const response = rule.matches(movie);
expect(response).toEqual(true);
assertRuleMatches(rule, movie);
});
it('should not match', async () => {
const rule = new Rule('simple', [new KeywordPredicate({ keyword: ['unknown keyword'] })], 'accept');
const response = rule.matches(movie);
expect(response).toEqual(false);
const rule = testRule(new KeywordPredicate({ keyword: ['unknown keyword'] }));
assertRuleDoesntMatch(rule, movie);
});
});
describe('adult predicate', () => {
it('should match true/false', async () => {
const noRule = testRule(new AdultPredicate({ adult: 'false' }));
const yesRule = testRule(new AdultPredicate({ adult: 'true' }));
assertRuleMatches(noRule, movie);
assertRuleDoesntMatch(yesRule, movie);
});
it('should match yes/no', async () => {
const noRule = testRule(new AdultPredicate({ adult: 'no' }));
const yesRule = testRule(new AdultPredicate({ adult: 'yes' }));
assertRuleMatches(noRule, movie);
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);
});
});
describe('originalLanguage predicate', () => {
it('should match', async () => {
assertRuleMatches(testRule(new OriginalLanguagePredicate({ originalLanguage: 'en' })), movie);
assertRuleMatches(testRule(new OriginalLanguagePredicate({ originalLanguage: ['en', 'fr'] })), movie);
});
it('should not match', async () => {
assertRuleDoesntMatch(testRule(new OriginalLanguagePredicate({ originalLanguage: 'fr' })), movie);
assertRuleDoesntMatch(testRule(new OriginalLanguagePredicate({ originalLanguage: ['fr', 'de'] })), movie);
});
});
describe('status predicate', () => {
it('should match', async () => {
assertRuleMatches(testRule(new StatusPredicate({ status: 'released' })), movie);
assertRuleMatches(testRule(new StatusPredicate({ status: ['released', 'post production'] })), movie);
});
it('should not match', async () => {
assertRuleDoesntMatch(testRule(new StatusPredicate({ status: 'canceled' })), movie);
assertRuleDoesntMatch(testRule(new StatusPredicate({ status: ['canceled', 'post production'] })), movie);
});
});

View File

@@ -13,6 +13,10 @@ import { KeywordPredicateBuilder } from '@core/lib/rules/predicate/keywords';
import { OrPredicateBuilder } from '@core/lib/rules/predicate/or';
import { AndPredicateBuilder } from '@core/lib/rules/predicate/and';
import { NotPredicateBuilder } from '@core/lib/rules/predicate/not';
import { AdultPredicateBuilder } from '@core/lib/rules/predicate/adult';
import { RuntimePredicateBuilder } from '@core/lib/rules/predicate/runtime';
import { OriginalLanguagePredicateBuilder } from '@core/lib/rules/predicate/originalLanguage';
import { StatusPredicateBuilder } from '@core/lib/rules/predicate/status';
export class PredicateFactoryClass {
private builders: Map<PredicateKey, PredicateBuilder>;
@@ -45,18 +49,22 @@ export class PredicateFactoryClass {
export const PredicateFactory = new PredicateFactoryClass();
const builders = [
OrPredicateBuilder,
AndPredicateBuilder,
NotPredicateBuilder,
AdultPredicateBuilder,
AgePredicateBuilder,
ScorePredicateBuilder,
VoteCountPredicateBuilder,
WatchProvidersPredicateBuilder,
GenrePredicateBuilder,
AndPredicateBuilder,
CastPredicateBuilder,
CrewPredicateBuilder,
GenrePredicateBuilder,
KeywordPredicateBuilder,
NotPredicateBuilder,
OriginalLanguagePredicateBuilder,
OrPredicateBuilder,
ProductionCompanyPredicateBuilder,
ReleasedPredicateBuilder,
KeywordPredicateBuilder,
RuntimePredicateBuilder,
ScorePredicateBuilder,
StatusPredicateBuilder,
VoteCountPredicateBuilder,
WatchProvidersPredicateBuilder,
];
builders.forEach((b) => PredicateFactory.registerBuilder(b));

View File

@@ -16,14 +16,18 @@ export type PredicateOption =
| AndFilterOptions
| OrFilterOptions
| NotFilterOptions
| AdultOptions
| AgeOptions
| CastOptions
| CrewOptions
| GenreOptions
| KeywordOptions
| OriginalLanguageOptions
| ProductionCompanyOptions
| ReleasedOptions
| RuntimeOptions
| ScoreOptions
| StatusOptions
| VoteCountOptions
| WatchProvidersOptions;
@@ -39,6 +43,10 @@ export interface NotFilterOptions {
not: PredicateOption[];
}
export type AdultOptions = {
adult: string;
};
export type AgeOptions = {
age: string;
};
@@ -51,6 +59,10 @@ export type GenreOptions = {
genre: string | string[];
};
export type OriginalLanguageOptions = {
originalLanguage: string | string[];
};
export type VoteCountOptions = {
voteCount: string;
};
@@ -88,3 +100,11 @@ export type CrewJobNamesOptions = {
job: string;
names: string[];
};
export type RuntimeOptions = {
runtime: string;
};
export type StatusOptions = {
status: string | string[];
};

View File

@@ -0,0 +1,21 @@
import { PredicateBuilder } from '@core/lib/rules';
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { BooleanPredicate } from '@core/lib/rules/predicate/boolean';
import { AdultOptions } from '@core/lib/rules/interfaces';
export class AdultPredicate extends BooleanPredicate {
constructor(options: AdultOptions) {
super({
value: options.adult,
});
}
matches(movie: MovieDetails): boolean {
return movie.adult == this.targetValue;
}
}
export const AdultPredicateBuilder: PredicateBuilder = {
key: 'adult',
build: (data: AdultOptions) => new AdultPredicate(data),
};

View File

@@ -0,0 +1,25 @@
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { PredicateBuilder } from '@core/lib/rules';
import TagsPredicate from '@core/lib/rules/predicate/tag';
import { OriginalLanguageOptions } from '@core/lib/rules/interfaces';
export class OriginalLanguagePredicate extends TagsPredicate {
constructor(options: OriginalLanguageOptions) {
super({
terms: Array.isArray(options.originalLanguage) ? options.originalLanguage : [options.originalLanguage],
});
}
getTags(movie: MovieDetails): string[] {
if (!movie.originalLanguage) {
return [];
}
return [movie.originalLanguage];
}
}
export const OriginalLanguagePredicateBuilder: PredicateBuilder = {
key: 'originalLanguage',
build: (data: OriginalLanguageOptions) => new OriginalLanguagePredicate(data),
};

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

@@ -0,0 +1,25 @@
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { PredicateBuilder } from '@core/lib/rules';
import TagsPredicate from '@core/lib/rules/predicate/tag';
import { StatusOptions } from '@core/lib/rules/interfaces';
export class StatusPredicate extends TagsPredicate {
constructor(options: StatusOptions) {
super({
terms: Array.isArray(options.status) ? options.status : [options.status],
});
}
getTags(movie: MovieDetails): string[] {
if (!movie.status) {
return [];
}
return [movie.status];
}
}
export const StatusPredicateBuilder: PredicateBuilder = {
key: 'status',
build: (data: StatusOptions) => new StatusPredicate(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,