209
README.md
209
README.md
@@ -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`
|
||||
|
||||
@@ -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" }}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
21
src/lib/rules/predicate/adult.ts
Normal file
21
src/lib/rules/predicate/adult.ts
Normal 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),
|
||||
};
|
||||
25
src/lib/rules/predicate/originalLanguage.ts
Normal file
25
src/lib/rules/predicate/originalLanguage.ts
Normal 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),
|
||||
};
|
||||
22
src/lib/rules/predicate/runtime.ts
Normal file
22
src/lib/rules/predicate/runtime.ts
Normal 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),
|
||||
};
|
||||
25
src/lib/rules/predicate/status.ts
Normal file
25
src/lib/rules/predicate/status.ts
Normal 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),
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user