Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/publish-beta-image.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Publish beta image to Docker Hub

on:
release:
types: [prereleased]
workflow_dispatch:

jobs:
publish_beta_image:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Log in to Docker Hub
run: |
echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u varthe --password-stdin

- name: Build and push multi-arch beta image
run: |
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 -t varthe/redirecterr:beta --push .
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ config/
# Configuration files
eslint.config.mjs
.prettierrc
*.yaml
.prettierignore
config.yaml
193 changes: 99 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

# Redirecterr

Filter and redirect Overseerr/Jellyseerr requests based on requester, keywords, age ratings, and more. Supports routing to multiple instances simultaneously.

## Getting Started

### Docker Compose
Expand All @@ -9,17 +10,17 @@ Use the following `docker-compose.yaml` to deploy the Redirecterr service:

```yaml
services:
redirecterr:
image: varthe/redirecterr:latest
container_name: redirecterr
hostname: redirecterr
ports:
- 8481:8481
volumes:
- /path/to/config:/config
- /path/to/logs:/logs
environment:
- LOG_LEVEL=info
redirecterr:
image: varthe/redirecterr:latest
container_name: redirecterr
hostname: redirecterr
ports:
- 8481:8481
volumes:
- /path/to/config:/config
- /path/to/logs:/logs
environment:
- LOG_LEVEL=info
```

### Webhook setup
Expand All @@ -28,70 +29,74 @@ In order for Redirecterr to work you need to disable automatic request approval

Then, in your seerr navigate to **Settings -> Notifications -> Webhook** and configure the following:

- **Enable Agent**: Enabled
- **Webhook URL**: `http://redirecterr:8481/webhook`
- **JSON Payload**:
```json
{
"notification_type": "{{notification_type}}",
"media": {
"media_type": "{{media_type}}",
"tmdbId": "{{media_tmdbid}}",
"tvdbId": "{{media_tvdbid}}",
"status": "{{media_status}}",
"status4k": "{{media_status4k}}"
},
"request": {
"request_id": "{{request_id}}",
"requestedBy_email": "{{requestedBy_email}}",
"requestedBy_username": "{{requestedBy_username}}",
"requestedBy_avatar": "{{requestedBy_avatar}}"
},
"{{extra}}": []
}
```
- **Notification Types**: Select **Request Pending Approval**
- **Enable Agent**: Enabled
- **Webhook URL**: `http://redirecterr:8481/webhook`
- **JSON Payload**:
```json
{
"notification_type": "{{notification_type}}",
"media": {
"media_type": "{{media_type}}",
"tmdbId": "{{media_tmdbid}}",
"tvdbId": "{{media_tvdbid}}",
"status": "{{media_status}}",
"status4k": "{{media_status4k}}"
},
"request": {
"request_id": "{{request_id}}",
"requestedBy_email": "{{requestedBy_email}}",
"requestedBy_username": "{{requestedBy_username}}",
"requestedBy_avatar": "{{requestedBy_avatar}}"
},
"{{extra}}": []
}
```
- **Notification Types**: Select **Request Pending Approval**

## Configuration Overview

The configuration for Redirecterr is defined in `config.yaml`. Below is a breakdown of the required and optional settings.

### Required Settings

- **`overseerr_url`**: The base URL of your Overseerr instance.
- **`overseerr_api_token`**: The API token for your Overseerr instance.
- **`overseerr_url`**: The base URL of your Overseerr instance.
- **`overseerr_api_token`**: The API token for your Overseerr instance.

### Fallback Settings
- **`approve_on_no_match`**: When no filters match, the request is approved automatically and handled by Overseerr according to its default settings. (Recommended)

- **`approve_on_no_match`**: When no filters match, the request is approved automatically and handled by Overseerr according to its default settings. (Recommended)

### Instances

Define your Radarr and Sonarr instances in this section. You can name the instances as needed.

- **`server_id`** (Required): The ID of the instance as shown in **Settings -> Services** in Overseerr. IDs start at 0 and increment sequentially from left to right (see image below).
- **`root_folder`** (Required): The path to the root folder for the instance, as configured in its settings.
- **`quality_profile_id`** (Optional): Overrides the default quality profile set in Overseerr. If not provided, the default profile will be used. To find the profile ID, open your browser and use the following URL, replacing `<url>` with your arr instance's URL and `<api-key>` with its API key:
```
http://<url>/api/v3/qualityProfile?apiKey=<api-key>
```
This returns a JSON response listing all available quality profiles and their IDs. The ID can be found at the very bottom of the response.
- **`approve`** (Optional): Automatically approves requests by default. To disable, set this flag to `False` in the configuration.
- **`server_id`** (Required): The ID of the instance as shown in **Settings -> Services** in Overseerr. IDs start at 0 and increment sequentially from left to right (see image below).
- **`root_folder`** (Required): The path to the root folder for the instance, as configured in its settings.
- **`quality_profile_id`** (Optional): Overrides the default quality profile set in Overseerr. If not provided, the default profile will be used. To find the profile ID, open your browser and use the following URL, replacing `<url>` with your arr instance's URL and `<api-key>` with its API key:
```
http://<url>/api/v3/qualityProfile?apiKey=<api-key>
```
This returns a JSON response listing all available quality profiles and their IDs. The ID can be found at the very bottom of the response.
- **`approve`** (Optional): Automatically approves requests by default. To disable, set this flag to `False` in the configuration.

![arrs](https://github.com/user-attachments/assets/a7a60d91-0f24-42a9-bbe1-ea4f1c945e6a)

### Filters

Define your request filters in this section.

- **`media_type`**: Specifies the type of media, either `"movie"` or `"tv"`.
- **`is_not_4k`**: Should only apply to non-4k requests
- **`is_4k`**: Should only apply to 4k requests
- **`conditions`**: A set of fields and values used to filter requests. Refer to [testData.js](https://github.com/varthe/Redirecterr/blob/main/testData.js) for examples of request objects. Each field within `conditions` can be:
- A **single value**: Matches if the value is present in the request.
- A **list of values**: Matches if any value in the list is present in the request.
- An **`exclude`** object: Used to exclude specific values. The `exclude`` object can contain either a single value or a list of values. The filter will match if none of the specified values are present in the request.
- **`media_type`**: Specifies the type of media, either `"movie"` or `"tv"`.
- **`is_not_4k`**: Should only apply to non-4k requests
- **`is_4k`**: Should only apply to 4k requests
- **`conditions`**: A set of fields and values used to filter requests. Refer to [testData.js](https://github.com/varthe/Redirecterr/blob/main/testData.js) for examples of request objects. Each field within `conditions` can be:

- A **single value**: Matches if the value is present in the request.
- A **list of values**: Matches if any value in the list is present in the request.
- A **`require`** object: Used to require specific values. The `require` object can contain either a single value or a list of values. The filter will match if all specified values are present in the request.
- An **`exclude`** object: Used to exclude specific values. The `exclude` object can contain either a single value or a list of values. The filter will match if none of the specified values are present in the request.

- **`apply`**: A list of instance names (defined in the **Instances** section) to which the request will be sent.

- **`apply`**: A list of instance names (defined in the **Instances** section) to which the request will be sent.

Redirecterr processes filters sequentially and will apply the first matching filter it encounters. Make sure to order your filters appropriately to get the desired behavior.

### Sample `config.yaml`
Expand All @@ -103,44 +108,44 @@ overseerr_api_token: "YOUR_API_TOKEN"
approve_on_no_match: True

instances:
radarr: # Custom instance name
server_id: 0
root_folder: "/mnt/plex/Movies"
radarr4k: # Custom instance name
server_id: 1
root_folder: "/mnt/plex/Movies - 4K"
radarr_anime: # Custom instance name
server_id: 2
root_folder: "/mnt/plex/Movies - Anime"
quality_profile_id: 2 # Optional
approve: false # Optional
sonarr: # Custom instance name
server_id: 0
root_folder: "/mnt/plex/Shows"
radarr: # Custom instance name
server_id: 0
root_folder: "/mnt/plex/Movies"
radarr4k: # Custom instance name
server_id: 1
root_folder: "/mnt/plex/Movies - 4K"
radarr_anime: # Custom instance name
server_id: 2
root_folder: "/mnt/plex/Movies - Anime"
quality_profile_id: 2 # Optional
approve: false # Optional
sonarr: # Custom instance name
server_id: 0
root_folder: "/mnt/plex/Shows"

filters:
- media_type: movie
conditions:
keywords: anime # Match if keyword "anime" is present
requestedBy_username: varthe # Match if requested by "varthe"
# requestedBy_email: ""
apply: radarr_anime # Send request to "radarr_anime"
- media_type: movie
conditions:
keywords: # Exclude requests with keywords "anime" or "animation"
exclude:
- anime
- animation
apply: # Send requests to "radarr" and "radarr4k"
- radarr
- radarr4k
- media_type: tv
conditions:
genres: # Match if genre is "adventure" or "comedy"
- adventure
- comedy
contentRatings: # Match if content rating is "12" or "16"
- 12
- 16
apply: sonarr # Send request to "sonarr"
- media_type: movie
conditions:
keywords: anime # Match if keyword "anime" is present
requestedBy_username: varthe # Match if requested by "varthe"
# requestedBy_email: ""
apply: radarr_anime # Send request to "radarr_anime"
- media_type: movie
conditions:
keywords: # Exclude requests with keywords "anime" or "animation"
exclude:
- anime
- animation
apply: # Send requests to "radarr" and "radarr4k"
- radarr
- radarr4k
- media_type: tv
conditions:
genres: # Match if genre is "adventure" or "comedy"
- adventure
- comedy
contentRatings: # Match if content rating is "12" or "16"
- 12
- 16
apply: sonarr # Send request to "sonarr"
```
10 changes: 10 additions & 0 deletions configBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ const schema = {
required: ["exclude"],
additionalProperties: false,
},
{
type: "object",
properties: {
require: {
anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
},
},
required: ["require"],
additionalProperties: false,
},
],
},
},
Expand Down
27 changes: 22 additions & 5 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const buildDebugLogMessage = (message, details = {}) => {
}

// Main functions
const matchValue = (filterValue, dataValue) => {
const matchValue = (filterValue, dataValue, required = false) => {
const arrayValues = normalizeToArray(filterValue)

if (isObject(dataValue)) {
Expand All @@ -63,14 +63,22 @@ const matchValue = (filterValue, dataValue) => {
if (
value.some((item) =>
arrayValues.some((filterVal) =>
Object.values(item).some((field) => String(field).toLowerCase().includes(filterVal))
Object.values(item).some((field) => {
if (required) return String(field).toLowerCase() === filterVal
return String(field).toLowerCase().includes(filterVal)
})
)
)
) {
return true
}
} else {
if (arrayValues.some((filterVal) => String(value).toLowerCase().includes(filterVal))) {
if (
arrayValues.some((filterVal) => {
if (required) return String(value).toLowerCase() === filterVal
return String(value).toLowerCase().includes(filterVal)
})
) {
return true
}
}
Expand All @@ -80,7 +88,10 @@ const matchValue = (filterValue, dataValue) => {
if (isObjectArray(dataValue)) {
return dataValue.some((item) =>
arrayValues.some((value) =>
Object.values(item).some((field) => String(field).toLowerCase().includes(value))
Object.values(item).some((field) => {
if (required) return String(field).toLowerCase() === value
return String(field).toLowerCase().includes(value)
})
)
)
}
Expand Down Expand Up @@ -113,7 +124,13 @@ const findMatchingInstances = (webhook, data, filters) => {
)
}

if (value.exclude ? matchValue(value.exclude, requestValue) : !matchValue(value, requestValue)) {
if (value.require && !matchValue(value.require, requestValue, true)) {
logger.debug(`Filter check for required key "${key}" did not match.`)
return false
} else if (value.exclude && matchValue(value.exclude, requestValue)) {
logger.debug(`Filter check for excluded key "${key}" did not match.`)
return false
} else if (!matchValue(value, requestValue)) {
logger.debug(`Filter check for key "${key}" did not match.`)
return false
}
Expand Down