diff --git a/.github/workflows/contribution.yaml b/.github/workflows/contribution.yaml index ee523cd..58a3168 100644 --- a/.github/workflows/contribution.yaml +++ b/.github/workflows/contribution.yaml @@ -24,6 +24,8 @@ jobs: run: npm run lint - name: Format run: npm run format + - name: Docs format + run: npm run format-docs - name: Build run: npm run build - name: Tests diff --git a/config/settings.sample.yaml b/config/settings.sample.yaml index ff129d0..4ef77a7 100644 --- a/config/settings.sample.yaml +++ b/config/settings.sample.yaml @@ -67,21 +67,23 @@ config: - name: Accept favorites actors whenMatch: - cast: - - Jenna Ortega - - Michael Keaton - - Denzel Washington - - Keanu Reeves - - Jessica Alba - - Halle Berry - - Sydney Sweeney - - Tom Hanks - - Samuel L. Jackson - - Morgan Freeman - - Anthony Hopkins - - Gal Gadot - - Tom Cruise - - Brad Pitt - - Matt Damon + voice: exclude + names: + - Jenna Ortega + - Michael Keaton + - Denzel Washington + - Keanu Reeves + - Jessica Alba + - Halle Berry + - Sydney Sweeney + - Tom Hanks + - Samuel L. Jackson + - Morgan Freeman + - Anthony Hopkins + - Gal Gadot + - Tom Cruise + - Brad Pitt + - Matt Damon action: accept - name: Reject declining actors whenMatch: diff --git a/docs/additional.md b/docs/additional.md index 5c3d49a..600a099 100644 --- a/docs/additional.md +++ b/docs/additional.md @@ -1,4 +1,3 @@ - # Schema The JSON Schema is available here: https://github.com/psyko-gh/overcrawlrr/blob/master/schema/schema.json diff --git a/docs/configuration.md b/docs/configuration.md index 769190a..24c02a6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -9,25 +9,27 @@ Overcrawlrr configuration is defined in the `settings.yaml` searched by default --- ### Overseerr authentication + Overcrawlrr need to authenticate to Overseerr to fetch movie information and create movie requests. It is recommended to create a [local user in Overseerr](https://docs.overseerr.dev/using-overseerr/users#creating-local-users) dedicated to Overcrawlrr. Doing so, you can have fine-grained control on its permissions: -- Allow/deny automatic requests validation, -- Allow/deny to request in 4K, -- Specify the languages and the region when discovering movies, -- And more... +- Allow/deny automatic requests validation, +- Allow/deny to request in 4K, +- Specify the languages and the region when discovering movies, +- And more... Once the user is created, you can fill these value in Overcrawlrr `settings.yaml`: + ```yaml title="settings.yaml" - config: +config: overseerr: - apiUrl: xxx # required - user: xxx # required - password: xxx # required - dryRun: true # Optional - dryRun will not send requests to Overseerr - # allowing you to test your rules + apiUrl: xxx # required + user: xxx # required + password: xxx # required + dryRun: true # Optional - dryRun will not send requests to Overseerr + # allowing you to test your rules ``` --- @@ -39,37 +41,38 @@ Once the user is created, you can fill these value in Overcrawlrr `settings.yaml This authentication is optional and only required if you're using the Smart Recommendations job To read data from your Plex library, you have to specify your Plex URL and token in `settings.yaml`: + ```yaml title="settings.yaml" - config: +config: # Only required when using Smart recommendations job plex: apiUrl: xxx plexToken: xxx ``` - --- ### Using environment variables You can refer to environment variables using the `{{ key }}` syntax. + ```yaml title="settings.yaml" - config: +config: overseerr: - apiUrl: xxx - user: '{{ OVERSEERR_USER }}' - password: '{{ OVERSEERR_PASSWORD }}' + apiUrl: xxx + user: '{{ OVERSEERR_USER }}' + password: '{{ OVERSEERR_PASSWORD }}' ``` These variables can be defined in the `docker-compose.yaml` ```yaml title="docker-compose.yaml" services: - overcrawlrr: - # ... - environment: - - OVERSEERR_USER= - - OVERSEERR_PASSWORD= + overcrawlrr: + # ... + environment: + - OVERSEERR_USER= + - OVERSEERR_PASSWORD= ``` --- diff --git a/docs/installation.md b/docs/installation.md index c1672fc..f27753f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,23 +4,21 @@ Using Overcrawlrr requires you to have a running instance of Overseerr - With docker-compose ```yaml services: - overcrawlrr: - image: ghcr.io/psyko-gh/overcrawlrr:latest - container_name: overcrawlrr - restart: unless-stopped - ports: - - 5056:5056 - environment: - - OVERSEERR_USER= - - OVERSEERR_PASSWORD= - volumes: - - /path/to/config:/config - + overcrawlrr: + image: ghcr.io/psyko-gh/overcrawlrr:latest + container_name: overcrawlrr + restart: unless-stopped + ports: + - 5056:5056 + environment: + - OVERSEERR_USER= + - OVERSEERR_PASSWORD= + volumes: + - /path/to/config:/config ``` With docker cli diff --git a/docs/jobs.md b/docs/jobs.md index e0dbf1f..340545d 100644 --- a/docs/jobs.md +++ b/docs/jobs.md @@ -10,19 +10,19 @@ It will fetch movies from Overseerr _(from the upcoming/popular/trending section ```yaml title="settings.yaml" config: - overseer: - # .... - discovery: - # Required - cron: '30 3 * * *' - # Required - the overseer streams to search through - # Possible values: upcoming, popular, trending - streams: - - upcoming - - popular - - trending - # Required - the name of the ruleset used to evaluate movies - ruleset: Ruleset name + overseer: + # .... + discovery: + # Required + cron: '30 3 * * *' + # Required - the overseer streams to search through + # Possible values: upcoming, popular, trending + streams: + - upcoming + - popular + - trending + # Required - the name of the ruleset used to evaluate movies + ruleset: Ruleset name ``` ## Smart recommendations @@ -37,19 +37,38 @@ Considering the movie you liked, it will crawl their recommended movies and appl ```yaml title="settings.yaml" config: - plex: - # .... - smartRecommendations: - # Required - cron: '1 14 * * *' - # The name of the plex library to use as a seed - plexLibrary: Films - # Your minimum personal rating to consider - minimumRating: 6.9 - ruleset: Ruleset name + plex: + # .... + smartRecommendations: + # Required + cron: '1 14 * * *' + # The name of the plex library to use as a seed + plexLibrary: Films + # Your minimum personal rating to consider + minimumRating: 6.9 + ruleset: Ruleset name ``` -## Checking output +## Testing jobs + +### Evaluating a ruleset + +You can also evaluate a ruleset against a specific movie, using the following URL: + +``` +http://localhost:5056/api/movies//evaluate/ +``` + +Simply replace `` and `` by the values matching your needs. + +To find the ID of a movie, navigate to the movie in Overseerr. The ID is the number is the URL. + +### Manually triggering jobs + +You can manually trigger the job by accessing the following URL (assuming Overcrawlrr is accessible at `http://localhost:5056`): + +- Discover job: `http://localhost:5056/api/discover` +- Smart recommendations job: `http://localhost:5056/api/smartRecommendations` Evaluation of the jobs are displayed in the container log. If you named your service `overcrawlrr`, you can see it with the following command: diff --git a/docs/predicates.md b/docs/predicates.md index 94158c5..10fe1ea 100644 --- a/docs/predicates.md +++ b/docs/predicates.md @@ -5,9 +5,9 @@ Filters on the adult status of the movie. ```yaml - - adult: yes - # or - - adult: no +- adult: yes + # or +- adult: no ``` --- @@ -19,25 +19,27 @@ Filters on the age of the movie. See [Duration expressions](#duration-expressions) for more details ```yaml - - age: less than 2 years - # or - - age: more than 6 months +- age: less than 2 years + # or +- age: more than 6 months ``` --- + ### `and` Predicate that will match if all of its predicate matches ```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 - - genre: +# 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 ``` --- + ### `cast` Filters based on the cast of the movie. Will match when one or more of the listed name matches. @@ -45,12 +47,22 @@ Filters based on the cast of the movie. Will match when one or more of the liste **Case insensitive** ```yaml - - cast: - - Denzel Washington - - Jessica Alba +- cast: + - Denzel Washington + - Jessica Alba +``` + +It is also possible to exclude an cast when it's performing voice only (in animation movies for example) + +```yaml +- cast: + voice: exclude + names: + - Scarlett Johansson ``` --- + ### `crew` Filters based on the crew of the movie. Will match when one or more of the listed name matches. @@ -58,21 +70,23 @@ Filters based on the crew of the movie. Will match when one or more of the liste **Case insensitive** ```yaml - - crew: - - James Cameron - - Hans Zimmer +- crew: + - James Cameron + - Hans Zimmer ``` It is also possible to specify the job + ```yaml - - crew: - job: director - names: +- crew: + job: director + names: - James Cameron - Steven Spielberg ``` --- + ### `genre` Filters on the genre of the movie. Will match when one or more of the listed genres matches the genre of the movie. @@ -80,25 +94,27 @@ Filters on the genre of the movie. Will match when one or more of the listed gen **Case insensitive** ```yaml - - genre: musical - # or with an array of values - - genre: - - animation - - romance +- genre: musical +# or with an array of values +- genre: + - animation + - romance ``` --- + ### `not` Predicate that invert the result of its child predicate ```yaml - - not: - - genre: +- not: + - genre: - animation ``` --- + ### `originalLanguage` Filters on the original language of the movie @@ -106,27 +122,29 @@ 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 +# ISO 639-1 format of the language (de, au, us, fr...) +- originalLanguage: en +# or with an array of values +- originalLanguage: + - en + - fr ``` --- + ### `or` Predicate that will match if any of its predicate matches ```yaml - # Will match if the movie is less than 2 years old OR if the movie score is above 8 - - or: - - age: less than 2 years - - score: above 8 +# Will match if the movie is less than 2 years old OR if the movie score is above 8 +- or: + - age: less than 2 years + - score: above 8 ``` --- + ### `productionCompany` Filters based on the production companies of the movie. Will match when one or more of the listed company matches. @@ -134,24 +152,26 @@ Filters based on the production companies of the movie. Will match when one or m **Case insensitive** ```yaml - - productionCompany: - - 20th Century Fox - - Warner Bros. Pictures - - Twisted Pictures +- productionCompany: + - 20th Century Fox + - Warner Bros. Pictures + - Twisted Pictures ``` --- + ### `released` Filters on the released status of the movie. ```yaml - - released: yes - # or - - released: no +- released: yes + # or +- released: no ``` --- + ### `runtime` Filters on the runtime _(duration)_ of the movie. @@ -159,12 +179,13 @@ 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 +- runtime: less than 2.5 hours + # or +- runtime: more than 120 minutes ``` --- + ### `score` Filters on the score of the movie. @@ -172,12 +193,13 @@ Filters on the score of the movie. Overseerr score is expressed between 0 to 10, but to make things clear, it is possible to pass the score as a fractional number. ```yaml - - score: above 6.5 - # or - - score: below 75/100 # Would be the same as 7.5, 7.5/10 or even 750/1000 +- score: above 6.5 + # or +- score: below 75/100 # Would be the same as 7.5, 7.5/10 or even 750/1000 ``` --- + ### `status` Filters on the status of the movie. @@ -187,24 +209,24 @@ The possible values are the one provided by [TMDB](https://www.themoviedb.org/): **Case insensitive** ```yaml - - status: released - # or with an array of values - - status: - - released - - post production - - planned +- 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 +- voteCount: above 1000 + # or +- voteCount: below 100 ``` --- @@ -216,11 +238,11 @@ Filters based on the available Streaming/VOD platforms. Will match when one or m **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: +# 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 ``` @@ -231,12 +253,12 @@ Filters based on the available Streaming/VOD platforms. Will match when one or m 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` +- 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` The following expressions are valid: -- `less than 1 hour`/`less than 1 hours`/`less than 60 minutes` -- `less than 3 hours`/`less than 3 hour`/`less than 180 minute` -- `more than 6 month` +- `less than 1 hour`/`less than 1 hours`/`less than 60 minutes` +- `less than 3 hours`/`less than 3 hour`/`less than 180 minute` +- `more than 6 month` diff --git a/docs/rulesets.md b/docs/rulesets.md index 95c5eb4..5c4fc28 100644 --- a/docs/rulesets.md +++ b/docs/rulesets.md @@ -8,24 +8,24 @@ A ruleset is a named group of rules. ```yaml title="settings.yaml" config: - # ... - rulesets: - # Required - the name of the ruleset - - name: Ruleset name - # Optional - the name of another ruleset. - # This ruleset will apply all the rules in the extended ruleset - # before applying its own rules - extends: Another ruleset - # Required - An array defining the rules of the ruleset - rules: - - # rule 1 - - # rule 2 - - # rule ... + # ... + rulesets: + # Required - the name of the ruleset + - name: Ruleset name + # Optional - the name of another ruleset. + # This ruleset will apply all the rules in the extended ruleset + # before applying its own rules + extends: Another ruleset + # Required - An array defining the rules of the ruleset + rules: + - # rule 1 + - # rule 2 + - # rule ... ``` -- The rules are applied in the declared order, -- When a rule matches, the rule's `action` is applied, and the evaluation of the ruleset stops. -- If a rule doesn't match, it is ignored, and the next rule is evaluated. +- The rules are applied in the declared order, +- When a rule matches, the rule's `action` is applied, and the evaluation of the ruleset stops. +- If a rule doesn't match, it is ignored, and the next rule is evaluated. --- @@ -36,25 +36,25 @@ A rule is a named group of predicates. A predicate is a simple operation allowing to test the property of a movie. ```yaml title="settings.yaml" - # Required - the name of the rule - - name: The rule name - # Required - an array defining the predicates used in the rule - whenMatch: - - # predicate 1 - - # predicate 2 - - # ... - - # predicate n - # Required - the action to apply when the rule matches - # Possible values: accept or reject - action: accept +# Required - the name of the rule +- name: The rule name + # Required - an array defining the predicates used in the rule + whenMatch: + - # predicate 1 + - # predicate 2 + - # ... + - # predicate n + # Required - the action to apply when the rule matches + # Possible values: accept or reject + action: accept ``` The rule will match if **all the predicates** in the `whenMatch` match. When matching: -- If `action: accept`, a request is sent to Overseerr to add the movie, -- If `action: reject`, the movie is ignored, and the evaluation of the ruleset stops for this movie. +- If `action: accept`, a request is sent to Overseerr to add the movie, +- If `action: reject`, the movie is ignored, and the evaluation of the ruleset stops for this movie. --- @@ -66,25 +66,26 @@ This is particularly useful for common rejection rules. ```yaml title="settings.yaml" config: - # ... - rulesets: - - name: exclude-netflix - rules: - - name: Exclude netflix movies - watchProviders: - - region: us - - names: - - Netflix - action: reject - - name: filter-movies - extends: exclude-netflix # movie will be evaluated against exclude-netflix rules first - rules: - - name: High score - whenMatch: - - age: less than 1 year - - score: above 9/10 - action: accept + # ... + rulesets: + - name: exclude-netflix + rules: + - name: Exclude netflix movies + watchProviders: + - region: us + - names: + - Netflix + action: reject + - name: filter-movies + extends: exclude-netflix # movie will be evaluated against exclude-netflix rules first + rules: + - name: High score + whenMatch: + - age: less than 1 year + - score: above 9/10 + action: accept ``` + ## Ruleset example The following ruleset will: @@ -93,18 +94,18 @@ The following ruleset will: 2. Request all movies _(that went through rule 1)_ released in the past year, with a score above 7 ```yaml title="settings.yaml" - rulesets: - - name: New great movie - rules: +rulesets: + - name: New great movie + rules: - name: Reject less wanted genres whenMatch: - - genre: - - animation - - romance + - genre: + - animation + - romance action: reject - name: New great movies whenMatch: - - age: less than 1 years - - score: above 7 + - age: less than 1 years + - score: above 7 action: accept ``` diff --git a/package.json b/package.json index e60b143..c115113 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "lint": "eslint \"./**/*.ts\"", "format:fix": "prettier ./src --write --cache", "format": "prettier ./src --check --cache", + "format-docs": "prettier ./docs --check --cache", + "format-docs:fix": "prettier ./docs --write --cache", "start": "tsx src/index.ts", "test": "jest" }, diff --git a/schema/schema.json b/schema/schema.json index fc747c4..f6bbc3b 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -218,7 +218,18 @@ "type": "object", "required": ["cast"], "properties": { - "cast": { "type": "array", "items": {"type": "string" }} + "cast": { + "oneOf": [ + { "type": "array", "items": { "type": "string" }}, + { + "type": "object", + "properties": { + "voice": { "type": "string", "enum": ["include", "exclude"]}, + "names": { "type": "array", "items": {"type": "string" }} + } + } + ] + } } }, diff --git a/src/lib/rules/__tests__/rules.ts b/src/lib/rules/__tests__/rules.ts index 70b479e..47521f7 100644 --- a/src/lib/rules/__tests__/rules.ts +++ b/src/lib/rules/__tests__/rules.ts @@ -157,6 +157,36 @@ describe('castPredicate', () => { const rule = testRule(new CastPredicate({ cast: ['Jessica Alba'] })); assertRuleDoesntMatch(rule, movie); }); + + it('should match with voice field', async () => { + const ruleIncludeVoice = testRule( + new CastPredicate({ + cast: { + voice: 'include', + names: ['Bob Sherman'], + }, + }) + ); + const ruleExcludeVoice = testRule( + new CastPredicate({ + cast: { + voice: 'exclude', + names: ['Bob Sherman'], + }, + }) + ); + const ruleExcludeVoice2 = testRule( + new CastPredicate({ + cast: { + voice: 'exclude', + names: ['Bob Sherman', 'Sigourney Weaver'], + }, + }) + ); + assertRuleMatches(ruleIncludeVoice, movie); + assertRuleMatches(ruleExcludeVoice2, movie); + assertRuleDoesntMatch(ruleExcludeVoice, movie); + }); }); describe('crewPredicate', () => { diff --git a/src/lib/rules/interfaces.ts b/src/lib/rules/interfaces.ts index 91de2d1..a4209a9 100644 --- a/src/lib/rules/interfaces.ts +++ b/src/lib/rules/interfaces.ts @@ -52,7 +52,12 @@ export type AgeOptions = { }; export type CastOptions = { - cast: string[]; + cast: string[] | CastWithVoiceOptions; +}; + +export type CastWithVoiceOptions = { + voice: 'include' | 'exclude'; + names: string[]; }; export type GenreOptions = { diff --git a/src/lib/rules/predicate/cast.ts b/src/lib/rules/predicate/cast.ts index 59c67cc..bec9979 100644 --- a/src/lib/rules/predicate/cast.ts +++ b/src/lib/rules/predicate/cast.ts @@ -4,10 +4,13 @@ import { PredicateBuilder } from '@core/lib/rules'; import { CastOptions } from '@core/lib/rules/interfaces'; export class CastPredicate extends TagsPredicate { + private excludeVoice: boolean = false; + constructor(options: CastOptions) { super({ - terms: options.cast, + terms: Array.isArray(options.cast) ? options.cast : options.cast.names, }); + this.excludeVoice = Array.isArray(options.cast) ? false : options.cast.voice?.toLowerCase() === 'exclude'; } getTags(movie: MovieDetails): string[] { @@ -16,7 +19,9 @@ export class CastPredicate extends TagsPredicate { } const tags: string[] = []; for (const cast of movie.credits.cast) { - tags.push(cast.name); + if (!this.excludeVoice || !cast.character || !cast.character.toLowerCase().includes('(voice)')) { + tags.push(cast.name); + } } return tags; }