Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a8b906b6f | |||
| b88ddde417 | |||
| 666c2656ec | |||
| 7786776ed8 | |||
| 2c60b38edf | |||
| 922f586706 |
@@ -105,7 +105,6 @@ _NCrunch_*
|
|||||||
_TeamCity*
|
_TeamCity*
|
||||||
|
|
||||||
# Sonarr
|
# Sonarr
|
||||||
config.xml
|
|
||||||
nzbdrone.log*txt
|
nzbdrone.log*txt
|
||||||
UpdateLogs/
|
UpdateLogs/
|
||||||
*workspace.xml
|
*workspace.xml
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 112 KiB |
@@ -21,8 +21,9 @@ Only the **latest versions** of the following apps are supported, or earlier ver
|
|||||||
- Transmission
|
- Transmission
|
||||||
- Sonarr
|
- Sonarr
|
||||||
- Radarr
|
- Radarr
|
||||||
|
- Lidarr
|
||||||
|
|
||||||
This tool is actively developed and still a work in progress. Join the Discord server if you want to reach out to me quickly (or just stay updated on new releases) so we can squash those pesky bugs together:
|
This tool is actively developed and still a work in progress, so using the `latest` Docker tag may result in breaking changes. Join the Discord server if you want to reach out to me quickly (or just stay updated on new releases) so we can squash those pesky bugs together:
|
||||||
|
|
||||||
> https://discord.gg/sWggpnmGNY
|
> https://discord.gg/sWggpnmGNY
|
||||||
|
|
||||||
@@ -35,8 +36,12 @@ This tool is actively developed and still a work in progress. Join the Discord s
|
|||||||
- Mark the files that were found in the queue as **unwanted/skipped** if:
|
- Mark the files that were found in the queue as **unwanted/skipped** if:
|
||||||
- They **are listed in the blacklist**, or
|
- They **are listed in the blacklist**, or
|
||||||
- They **are not included in the whitelist**.
|
- They **are not included in the whitelist**.
|
||||||
|
- If **all files** of a download **are unwanted**:
|
||||||
|
- It will be removed from the *arr's queue and blocked.
|
||||||
|
- It will be deleted from the download client.
|
||||||
|
- A new search will be triggered for the *arr item.
|
||||||
2. **Queue cleaner** will:
|
2. **Queue cleaner** will:
|
||||||
- Run every 5 minutes (or configured cron).
|
- Run every 5 minutes (or configured cron, or right after `content blocker`).
|
||||||
- Process all items in the *arr queue.
|
- Process all items in the *arr queue.
|
||||||
- Check each queue item if it is **stalled (download speed is 0)**, **stuck in matadata downloading** or **failed to be imported**.
|
- Check each queue item if it is **stalled (download speed is 0)**, **stuck in matadata downloading** or **failed to be imported**.
|
||||||
- If it is, the item receives a **strike** and will continue to accumulate strikes every time it meets any of these conditions.
|
- If it is, the item receives a **strike** and will continue to accumulate strikes every time it meets any of these conditions.
|
||||||
@@ -63,8 +68,8 @@ This tool is actively developed and still a work in progress. Join the Discord s
|
|||||||
|
|
||||||
## Using cleanuperr's blocklist (works with all supported download clients)
|
## Using cleanuperr's blocklist (works with all supported download clients)
|
||||||
|
|
||||||
1. Set both `QUEUECLEANER__ENABLED` and `CONTENTBLOCKER_ENABLED` to `true` in your environment variables.
|
1. Set both `QUEUECLEANER__ENABLED` and `CONTENTBLOCKER__ENABLED` to `true` in your environment variables.
|
||||||
2. Configure and enable either a **blacklist** or a **whitelist** as described in the [Environment variables](#Environment-variables) section.
|
2. Configure and enable either a **blacklist** or a **whitelist** as described in the [Arr variables](#Arr-variables) section.
|
||||||
3. Once configured, cleanuperr will perform the following tasks:
|
3. Once configured, cleanuperr will perform the following tasks:
|
||||||
- Execute the **content blocker** job, as explained in the [How it works](#how-it-works) section.
|
- Execute the **content blocker** job, as explained in the [How it works](#how-it-works) section.
|
||||||
- Execute the **queue cleaner** job, as explained in the [How it works](#how-it-works) section.
|
- Execute the **queue cleaner** job, as explained in the [How it works](#how-it-works) section.
|
||||||
@@ -101,23 +106,23 @@ services:
|
|||||||
- QUEUECLEANER__RUNSEQUENTIALLY=true
|
- QUEUECLEANER__RUNSEQUENTIALLY=true
|
||||||
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
|
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
|
||||||
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=false
|
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=false
|
||||||
|
- QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE=false
|
||||||
# - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=title mismatch
|
# - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=title mismatch
|
||||||
# - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1=manual import required
|
# - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1=manual import required
|
||||||
- QUEUECLEANER__STALLED_MAX_STRIKES=5
|
- QUEUECLEANER__STALLED_MAX_STRIKES=5
|
||||||
- QUEUECLEANER__STALLED_IGNORE_PRIVATE=false
|
- QUEUECLEANER__STALLED_IGNORE_PRIVATE=false
|
||||||
|
- QUEUECLEANER__STALLED_DELETE_PRIVATE=false
|
||||||
|
|
||||||
- CONTENTBLOCKER__ENABLED=true
|
- CONTENTBLOCKER__ENABLED=true
|
||||||
- CONTENTBLOCKER__IGNORE_PRIVATE=true
|
- CONTENTBLOCKER__IGNORE_PRIVATE=false
|
||||||
- CONTENTBLOCKER__BLACKLIST__ENABLED=true
|
- CONTENTBLOCKER__DELETE_PRIVATE=false
|
||||||
- CONTENTBLOCKER__BLACKLIST__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist
|
|
||||||
# OR
|
|
||||||
# - CONTENTBLOCKER__WHITELIST__ENABLED=true
|
|
||||||
# - CONTENTBLOCKER__WHITELIST__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/whitelist
|
|
||||||
|
|
||||||
- DOWNLOAD_CLIENT=qBittorrent
|
- DOWNLOAD_CLIENT=none
|
||||||
- QBITTORRENT__URL=http://localhost:8080
|
# OR
|
||||||
- QBITTORRENT__USERNAME=user
|
# - DOWNLOAD_CLIENT=qBittorrent
|
||||||
- QBITTORRENT__PASSWORD=pass
|
# - QBITTORRENT__URL=http://localhost:8080
|
||||||
|
# - QBITTORRENT__USERNAME=user
|
||||||
|
# - QBITTORRENT__PASSWORD=pass
|
||||||
# OR
|
# OR
|
||||||
# - DOWNLOAD_CLIENT=deluge
|
# - DOWNLOAD_CLIENT=deluge
|
||||||
# - DELUGE__URL=http://localhost:8112
|
# - DELUGE__URL=http://localhost:8112
|
||||||
@@ -127,53 +132,73 @@ services:
|
|||||||
# - TRANSMISSION__URL=http://localhost:9091
|
# - TRANSMISSION__URL=http://localhost:9091
|
||||||
# - TRANSMISSION__USERNAME=test
|
# - TRANSMISSION__USERNAME=test
|
||||||
# - TRANSMISSION__PASSWORD=testing
|
# - TRANSMISSION__PASSWORD=testing
|
||||||
# OR
|
|
||||||
# - DOWNLOAD_CLIENT=none
|
|
||||||
|
|
||||||
- SONARR__ENABLED=true
|
- SONARR__ENABLED=true
|
||||||
- SONARR__SEARCHTYPE=Episode
|
- SONARR__SEARCHTYPE=Episode
|
||||||
|
- SONARR__BLOCK__TYPE=blacklist
|
||||||
|
- SONARR__BLOCK__PATH=https://example.com/path/to/file.txt
|
||||||
- SONARR__INSTANCES__0__URL=http://localhost:8989
|
- SONARR__INSTANCES__0__URL=http://localhost:8989
|
||||||
- SONARR__INSTANCES__0__APIKEY=secret1
|
- SONARR__INSTANCES__0__APIKEY=secret1
|
||||||
- SONARR__INSTANCES__1__URL=http://localhost:8990
|
- SONARR__INSTANCES__1__URL=http://localhost:8990
|
||||||
- SONARR__INSTANCES__1__APIKEY=secret2
|
- SONARR__INSTANCES__1__APIKEY=secret2
|
||||||
|
|
||||||
- RADARR__ENABLED=true
|
- RADARR__ENABLED=true
|
||||||
|
- RADARR__BLOCK__TYPE=blacklist
|
||||||
|
- RADARR__BLOCK__PATH=https://example.com/path/to/file.txt
|
||||||
- RADARR__INSTANCES__0__URL=http://localhost:7878
|
- RADARR__INSTANCES__0__URL=http://localhost:7878
|
||||||
- RADARR__INSTANCES__0__APIKEY=secret3
|
- RADARR__INSTANCES__0__APIKEY=secret3
|
||||||
- RADARR__INSTANCES__1__URL=http://localhost:7879
|
- RADARR__INSTANCES__1__URL=http://localhost:7879
|
||||||
- RADARR__INSTANCES__1__APIKEY=secret4
|
- RADARR__INSTANCES__1__APIKEY=secret4
|
||||||
|
|
||||||
|
- LIDARR__ENABLED=true
|
||||||
|
- LIDARR__BLOCK__TYPE=blacklist
|
||||||
|
- LIDARR__BLOCK__PATH=https://example.com/path/to/file.txt
|
||||||
|
- LIDARR__INSTANCES__0__URL=http://radarr:8686
|
||||||
|
- LIDARR__INSTANCES__0__APIKEY=secret5
|
||||||
|
- LIDARR__INSTANCES__1__URL=http://radarr:8687
|
||||||
|
- LIDARR__INSTANCES__1__APIKEY=secret6
|
||||||
image: ghcr.io/flmorg/cleanuperr:latest
|
image: ghcr.io/flmorg/cleanuperr:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment variables
|
## Environment variables
|
||||||
|
|
||||||
|
### General variables
|
||||||
|
<details>
|
||||||
|
<summary>Click here</summary>
|
||||||
|
|
||||||
| Variable | Required | Description | Default value |
|
| Variable | Required | Description | Default value |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| LOGGING__LOGLEVEL | No | Can be `Verbose`, `Debug`, `Information`, `Warning`, `Error` or `Fatal` | `Information` |
|
| LOGGING__LOGLEVEL | No | Can be `Verbose`, `Debug`, `Information`, `Warning`, `Error` or `Fatal`. | `Information` |
|
||||||
| LOGGING__FILE__ENABLED | No | Enable or disable logging to file | false |
|
| LOGGING__FILE__ENABLED | No | Enable or disable logging to file. | false |
|
||||||
| LOGGING__FILE__PATH | No | Directory where to save the log files | empty |
|
| LOGGING__FILE__PATH | No | Directory where to save the log files. | empty |
|
||||||
| LOGGING__ENHANCED | No | Enhance logs whenever possible<br>A more detailed description is provided [here](variables.md#LOGGING__ENHANCED) | true |
|
| LOGGING__ENHANCED | No | Enhance logs whenever possible.<br>A more detailed description is provided. [here](variables.md#LOGGING__ENHANCED) | true |
|
||||||
|||||
|
|||||
|
||||||
| TRIGGERS__QUEUECLEANER | Yes if queue cleaner is enabled | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html)<br>Can be a max of 6h interval<br>**Is ignored if `QUEUECLEANER__RUNSEQUENTIALLY=true` and `CONTENTBLOCKER__ENABLED=true`** | 0 0/5 * * * ? |
|
| TRIGGERS__QUEUECLEANER | Yes if queue cleaner is enabled | - [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html).<br>- Can be a max of 6h interval.<br>- **Is ignored if `QUEUECLEANER__RUNSEQUENTIALLY=true` and `CONTENTBLOCKER__ENABLED=true`**. | 0 0/5 * * * ? |
|
||||||
| TRIGGERS__CONTENTBLOCKER | Yes if content blocker is enabled | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html)<br>Can be a max of 6h interval | 0 0/5 * * * ? |
|
| TRIGGERS__CONTENTBLOCKER | Yes if content blocker is enabled | - [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html).<br>- Can be a max of 6h interval. | 0 0/5 * * * ? |
|
||||||
|||||
|
|||||
|
||||||
| QUEUECLEANER__ENABLED | No | Enable or disable the queue cleaner | true |
|
| QUEUECLEANER__ENABLED | No | Enable or disable the queue cleaner. | true |
|
||||||
| QUEUECLEANER__RUNSEQUENTIALLY | No | If set to true, the queue cleaner will run after the content blocker instead of running in parallel, streamlining the cleaning process | true |
|
| QUEUECLEANER__RUNSEQUENTIALLY | No | If set to true, the queue cleaner will run after the content blocker instead of running in parallel, streamlining the cleaning process. | true |
|
||||||
| QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES | No | After how many strikes should a failed import be removed<br>0 means never | 0 |
|
| QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES | No | - After how many strikes should a failed import be removed.<br>- 0 means never. | 0 |
|
||||||
| QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE | No | Whether to ignore failed imports from private trackers | false |
|
| QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE | No | Whether to ignore failed imports from private trackers. | false |
|
||||||
| QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0 | No | First pattern to look for when an import is failed<br>If the specified message pattern is found, the item is skipped | empty |
|
| QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE | No | - Whether to delete failed imports from the download client.<br>- Does not have any effect if `QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE` is `true`.<br>- **Set this to `true` if you don't care about seeding, ratio, H&R and potentially losing your tracker account.** | false |
|
||||||
| QUEUECLEANER__STALLED_MAX_STRIKES | No | After how many strikes should a stalled download be removed<br>0 means never | 0 |
|
| QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0 | No | - First pattern to look for when an import is failed.<br>- If the specified message pattern is found, the item is skipped. | empty |
|
||||||
| QUEUECLEANER__STALLED_IGNORE_PRIVATE | No | Whether to ignore stalled downloads from private trackers | false |
|
| QUEUECLEANER__STALLED_MAX_STRIKES | No | - After how many strikes should a stalled download be removed.<br>- 0 means never. | 0 |
|
||||||
|
| QUEUECLEANER__STALLED_IGNORE_PRIVATE | No | Whether to ignore stalled downloads from private trackers. | false |
|
||||||
|
| QUEUECLEANER__STALLED_DELETE_PRIVATE | No | - Whether to delete stalled downloads from the download client.<br>- Does not have any effect if `QUEUECLEANER__STALLED_IGNORE_PRIVATE` is `true`.<br>- **Set this to `true` if you don't care about seeding, ratio, H&R and potentially losing your tracker account.** | false |
|
||||||
|||||
|
|||||
|
||||||
| CONTENTBLOCKER__ENABLED | No | Enable or disable the content blocker | false |
|
| CONTENTBLOCKER__ENABLED | No | Enable or disable the content blocker. | false |
|
||||||
| CONTENTBLOCKER__IGNORE_PRIVATE | No | Whether to ignore downloads from private trackers | false |
|
| CONTENTBLOCKER__IGNORE_PRIVATE | No | Whether to ignore downloads from private trackers. | false |
|
||||||
| CONTENTBLOCKER__BLACKLIST__ENABLED | Yes if content blocker is enabled and whitelist is not enabled | Enable or disable the blacklist | false |
|
| CONTENTBLOCKER__DELETE_PRIVATE | No | - Whether to delete items that have all files blocked from the download client.<br>- Does not have any effect if `CONTENTBLOCKER__IGNORE_PRIVATE` is `true`.<br>- **Set this to `true` if you don't care about seeding, ratio, H&R and potentially losing your tracker account.** | false |
|
||||||
| CONTENTBLOCKER__BLACKLIST__PATH | Yes if blacklist is enabled | Path to the blacklist (local file or url)<br>Needs to be json compatible | empty |
|
</details>
|
||||||
| CONTENTBLOCKER__WHITELIST__ENABLED | Yes if content blocker is enabled and blacklist is not enabled | Enable or disable the whitelist | false |
|
|
||||||
| CONTENTBLOCKER__WHITELIST__PATH | Yes if whitelist is enabled | Path to the whitelist (local file or url)<br>Needs to be json compatible | empty |
|
### Download client variables
|
||||||
|||||
|
<details>
|
||||||
| DOWNLOAD_CLIENT | No | Download client that is used by *arrs<br>Can be `qbittorrent`, `deluge`, `transmission` or `none` | `qbittorrent` |
|
<summary>Click here</summary>
|
||||||
|
|
||||||
|
| Variable | Required | Description | Default value |
|
||||||
|
|---|---|---|---|
|
||||||
|
| DOWNLOAD_CLIENT | No | Download client that is used by *arrs<br>Can be `qbittorrent`, `deluge`, `transmission` or `none` | `none` |
|
||||||
| QBITTORRENT__URL | No | qBittorrent instance url | http://localhost:8112 |
|
| QBITTORRENT__URL | No | qBittorrent instance url | http://localhost:8112 |
|
||||||
| QBITTORRENT__USERNAME | No | qBittorrent user | empty |
|
| QBITTORRENT__USERNAME | No | qBittorrent user | empty |
|
||||||
| QBITTORRENT__PASSWORD | No | qBittorrent password | empty |
|
| QBITTORRENT__PASSWORD | No | qBittorrent password | empty |
|
||||||
@@ -184,42 +209,68 @@ services:
|
|||||||
| TRANSMISSION__URL | No | Transmission instance url | http://localhost:9091 |
|
| TRANSMISSION__URL | No | Transmission instance url | http://localhost:9091 |
|
||||||
| TRANSMISSION__USERNAME | No | Transmission user | empty |
|
| TRANSMISSION__USERNAME | No | Transmission user | empty |
|
||||||
| TRANSMISSION__PASSWORD | No | Transmission password | empty |
|
| TRANSMISSION__PASSWORD | No | Transmission password | empty |
|
||||||
|||||
|
</details>
|
||||||
| SONARR__ENABLED | No | Enable or disable Sonarr cleanup | true |
|
|
||||||
|
### Arr variables
|
||||||
|
<details>
|
||||||
|
<summary>Click here</summary>
|
||||||
|
|
||||||
|
| Variable | Required | Description | Default value |
|
||||||
|
|---|---|---|---|
|
||||||
|
| SONARR__ENABLED | No | Enable or disable Sonarr cleanup | false |
|
||||||
|
| SONARR__BLOCK__TYPE | No | Block type<br>Can be `blacklist` or `whitelist` | `blacklist` |
|
||||||
|
| SONARR__BLOCK__PATH | No | Path to the blocklist (local file or url)<br>Needs to be json compatible | empty |
|
||||||
| SONARR__SEARCHTYPE | No | What to search for after removing a queue item<br>Can be `Episode`, `Season` or `Series` | `Episode` |
|
| SONARR__SEARCHTYPE | No | What to search for after removing a queue item<br>Can be `Episode`, `Season` or `Series` | `Episode` |
|
||||||
| SONARR__INSTANCES__0__URL | No | First Sonarr instance url | http://localhost:8989 |
|
| SONARR__INSTANCES__0__URL | No | First Sonarr instance url | http://localhost:8989 |
|
||||||
| SONARR__INSTANCES__0__APIKEY | No | First Sonarr instance API key | empty |
|
| SONARR__INSTANCES__0__APIKEY | No | First Sonarr instance API key | empty |
|
||||||
|||||
|
|||||
|
||||||
| RADARR__ENABLED | No | Enable or disable Radarr cleanup | false |
|
| RADARR__ENABLED | No | Enable or disable Radarr cleanup | false |
|
||||||
| RADARR__INSTANCES__0__URL | No | First Radarr instance url | http://localhost:8989 |
|
| RADARR__BLOCK__TYPE | No | Block type<br>Can be `blacklist` or `whitelist` | `blacklist` |
|
||||||
|
| RADARR__BLOCK__PATH | No | Path to the blocklist (local file or url)<br>Needs to be json compatible | empty |
|
||||||
|
| RADARR__INSTANCES__0__URL | No | First Radarr instance url | http://localhost:7878 |
|
||||||
| RADARR__INSTANCES__0__APIKEY | No | First Radarr instance API key | empty |
|
| RADARR__INSTANCES__0__APIKEY | No | First Radarr instance API key | empty |
|
||||||
|||||
|
|||||
|
||||||
|
| LIDARR__ENABLED | No | Enable or disable LIDARR cleanup | false |
|
||||||
|
| LIDARR__BLOCK__TYPE | No | Block type<br>Can be `blacklist` or `whitelist` | `blacklist` |
|
||||||
|
| LIDARR__BLOCK__PATH | No | Path to the blocklist (local file or url)<br>Needs to be json compatible | empty |
|
||||||
|
| LIDARR__INSTANCES__0__URL | No | First LIDARR instance url | http://localhost:8686 |
|
||||||
|
| LIDARR__INSTANCES__0__APIKEY | No | First LIDARR instance API key | empty |
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Advanced variables
|
||||||
|
<details>
|
||||||
|
<summary>Click here</summary>
|
||||||
|
|
||||||
|
| Variable | Required | Description | Default value |
|
||||||
|
|---|---|---|---|
|
||||||
| HTTP_MAX_RETRIES | No | The number of times to retry a failed HTTP call (to *arrs, download clients etc.) | 0 |
|
| HTTP_MAX_RETRIES | No | The number of times to retry a failed HTTP call (to *arrs, download clients etc.) | 0 |
|
||||||
| HTTP_TIMEOUT | No | The number of seconds to wait before failing an HTTP call (to *arrs, download clients etc.) | 100 |
|
| HTTP_TIMEOUT | No | The number of seconds to wait before failing an HTTP call (to *arrs, download clients etc.) | 100 |
|
||||||
|
</details>
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
||||||
### To be noted
|
### To be noted
|
||||||
|
|
||||||
1. The blacklist and the whitelist can not be both enabled at the same time.
|
1. The queue cleaner and content blocker can be enabled or disabled separately, if you want to run only one of them.
|
||||||
2. The queue cleaner and content blocker can be enabled or disabled separately, if you want to run only one of them.
|
2. Only one download client can be enabled at a time. If you have more than one download client, you should deploy multiple instances of cleanuperr.
|
||||||
3. Only one download client can be enabled at a time. If you have more than one download client, you should deploy multiple instances of cleanuperr.
|
3. The blocklists (blacklist/whitelist) should have a single pattern on each line and supports the following:
|
||||||
4. The blocklists (blacklist/whitelist) should have a single pattern on each line and supports the following:
|
|
||||||
```
|
```
|
||||||
*example // file name ends with "example"
|
*example // file name ends with "example"
|
||||||
example* // file name starts with "example"
|
example* // file name starts with "example"
|
||||||
*example* // file name has "example" in the name
|
*example* // file name has "example" in the name
|
||||||
example // file name is exactly the word "example"
|
example // file name is exactly the word "example"
|
||||||
regex:<ANY_REGEX> // regex that needs to be marked at the start of the line with "regex:"
|
regex:<ANY_REGEX> // regex that needs to be marked at the start of the line with "regex:"
|
||||||
```
|
```
|
||||||
5. Multiple Sonarr/Radarr instances can be specified using this format, where `<NUMBER>` starts from 0:
|
4. Multiple Sonarr/Radarr/Lidarr instances can be specified using this format, where `<NUMBER>` starts from `0`:
|
||||||
```
|
```
|
||||||
SONARR__INSTANCES__<NUMBER>__URL
|
SONARR__INSTANCES__<NUMBER>__URL
|
||||||
SONARR__INSTANCES__<NUMBER>__APIKEY
|
SONARR__INSTANCES__<NUMBER>__APIKEY
|
||||||
```
|
```
|
||||||
6. Multiple failed import patterns can be specified using this format, where `<NUMBER>` starts from 0:
|
5. Multiple failed import patterns can be specified using this format, where `<NUMBER>` starts from 0:
|
||||||
```
|
```
|
||||||
QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__<NUMBER>
|
QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__<NUMBER>
|
||||||
```
|
```
|
||||||
|
6. [This blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist) and [this whitelist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/whitelist) can be used for Sonarr and Radarr, but they are not suitable for other *arrs.
|
||||||
#
|
#
|
||||||
|
|
||||||
### Binaries (if you're not using Docker)
|
### Binaries (if you're not using Docker)
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
|
using Common.Configuration.ContentBlocker;
|
||||||
|
|
||||||
namespace Common.Configuration.Arr;
|
namespace Common.Configuration.Arr;
|
||||||
|
|
||||||
public abstract record ArrConfig
|
public abstract record ArrConfig
|
||||||
{
|
{
|
||||||
public required bool Enabled { get; init; }
|
public required bool Enabled { get; init; }
|
||||||
|
|
||||||
|
public Block Block { get; init; } = new();
|
||||||
|
|
||||||
public required List<ArrInstance> Instances { get; init; }
|
public required List<ArrInstance> Instances { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Block
|
||||||
|
{
|
||||||
|
public BlocklistType Type { get; set; }
|
||||||
|
|
||||||
|
public string? Path { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Common.Configuration.Arr;
|
||||||
|
|
||||||
|
public sealed record LidarrConfig : ArrConfig
|
||||||
|
{
|
||||||
|
public const string SectionName = "Lidarr";
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Domain.Enums;
|
namespace Common.Configuration.ContentBlocker;
|
||||||
|
|
||||||
public enum BlocklistType
|
public enum BlocklistType
|
||||||
{
|
{
|
||||||
@@ -11,35 +11,10 @@ public sealed record ContentBlockerConfig : IJobConfig
|
|||||||
[ConfigurationKeyName("IGNORE_PRIVATE")]
|
[ConfigurationKeyName("IGNORE_PRIVATE")]
|
||||||
public bool IgnorePrivate { get; init; }
|
public bool IgnorePrivate { get; init; }
|
||||||
|
|
||||||
public PatternConfig? Blacklist { get; init; }
|
[ConfigurationKeyName("DELETE_PRIVATE")]
|
||||||
|
public bool DeletePrivate { get; init; }
|
||||||
|
|
||||||
public PatternConfig? Whitelist { get; init; }
|
|
||||||
|
|
||||||
public void Validate()
|
public void Validate()
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Blacklist is null && Whitelist is null)
|
|
||||||
{
|
|
||||||
throw new Exception("content blocker is enabled, but both blacklist and whitelist are missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Blacklist?.Enabled is true && Whitelist?.Enabled is true)
|
|
||||||
{
|
|
||||||
throw new Exception("only one exclusion (blacklist/whitelist) list is allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Blacklist?.Enabled is true && string.IsNullOrEmpty(Blacklist.Path))
|
|
||||||
{
|
|
||||||
throw new Exception("blacklist path is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Whitelist?.Enabled is true && string.IsNullOrEmpty(Whitelist.Path))
|
|
||||||
{
|
|
||||||
throw new Exception("blacklist path is required");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Common.Configuration.ContentBlocker;
|
|
||||||
|
|
||||||
public sealed record PatternConfig
|
|
||||||
{
|
|
||||||
public bool Enabled { get; init; }
|
|
||||||
|
|
||||||
public string? Path { get; init; }
|
|
||||||
}
|
|
||||||
@@ -5,5 +5,5 @@ namespace Common.Configuration.DownloadClient;
|
|||||||
public sealed record DownloadClientConfig
|
public sealed record DownloadClientConfig
|
||||||
{
|
{
|
||||||
[ConfigurationKeyName("DOWNLOAD_CLIENT")]
|
[ConfigurationKeyName("DOWNLOAD_CLIENT")]
|
||||||
public Enums.DownloadClient DownloadClient { get; init; } = Enums.DownloadClient.QBittorrent;
|
public Enums.DownloadClient DownloadClient { get; init; } = Enums.DownloadClient.None;
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,9 @@ public sealed record QueueCleanerConfig : IJobConfig
|
|||||||
[ConfigurationKeyName("IMPORT_FAILED_IGNORE_PRIVATE")]
|
[ConfigurationKeyName("IMPORT_FAILED_IGNORE_PRIVATE")]
|
||||||
public bool ImportFailedIgnorePrivate { get; init; }
|
public bool ImportFailedIgnorePrivate { get; init; }
|
||||||
|
|
||||||
|
[ConfigurationKeyName("IMPORT_FAILED_DELETE_PRIVATE")]
|
||||||
|
public bool ImportFailedDeletePrivate { get; init; }
|
||||||
|
|
||||||
[ConfigurationKeyName("IMPORT_FAILED_IGNORE_PATTERNS")]
|
[ConfigurationKeyName("IMPORT_FAILED_IGNORE_PATTERNS")]
|
||||||
public List<string>? ImportFailedIgnorePatterns { get; init; }
|
public List<string>? ImportFailedIgnorePatterns { get; init; }
|
||||||
|
|
||||||
@@ -24,6 +27,9 @@ public sealed record QueueCleanerConfig : IJobConfig
|
|||||||
|
|
||||||
[ConfigurationKeyName("STALLED_IGNORE_PRIVATE")]
|
[ConfigurationKeyName("STALLED_IGNORE_PRIVATE")]
|
||||||
public bool StalledIgnorePrivate { get; init; }
|
public bool StalledIgnorePrivate { get; init; }
|
||||||
|
|
||||||
|
[ConfigurationKeyName("STALLED_DELETE_PRIVATE")]
|
||||||
|
public bool StalledDeletePrivate { get; init; }
|
||||||
|
|
||||||
public void Validate()
|
public void Validate()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Domain.Models.Arr.Blocking;
|
||||||
|
|
||||||
|
public record BlockedItem
|
||||||
|
{
|
||||||
|
public required string Hash { get; init; }
|
||||||
|
|
||||||
|
public required Uri InstanceUrl { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Domain.Models.Arr.Blocking;
|
||||||
|
|
||||||
|
public sealed record LidarrBlockedItem : BlockedItem
|
||||||
|
{
|
||||||
|
public required long AlbumId { get; init; }
|
||||||
|
|
||||||
|
public required long ArtistId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Domain.Models.Arr.Blocking;
|
||||||
|
|
||||||
|
public sealed record RadarrBlockedItem : BlockedItem
|
||||||
|
{
|
||||||
|
public required long MovieId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Domain.Models.Arr.Blocking;
|
||||||
|
|
||||||
|
public sealed record SonarrBlockedItem : BlockedItem
|
||||||
|
{
|
||||||
|
public required long EpisodeId { get; init; }
|
||||||
|
|
||||||
|
public required long SeasonNumber { get; init; }
|
||||||
|
|
||||||
|
public required long SeriesId { get; init; }
|
||||||
|
}
|
||||||
@@ -2,10 +2,20 @@ namespace Domain.Models.Arr.Queue;
|
|||||||
|
|
||||||
public sealed record QueueRecord
|
public sealed record QueueRecord
|
||||||
{
|
{
|
||||||
public int SeriesId { get; init; }
|
// Sonarr
|
||||||
public int EpisodeId { get; init; }
|
public long SeriesId { get; init; }
|
||||||
public int SeasonNumber { get; init; }
|
public long EpisodeId { get; init; }
|
||||||
public int MovieId { get; init; }
|
public long SeasonNumber { get; init; }
|
||||||
|
|
||||||
|
// Radarr
|
||||||
|
public long MovieId { get; init; }
|
||||||
|
|
||||||
|
// Lidarr
|
||||||
|
public long ArtistId { get; init; }
|
||||||
|
|
||||||
|
public long AlbumId { get; init; }
|
||||||
|
|
||||||
|
// common
|
||||||
public required string Title { get; init; }
|
public required string Title { get; init; }
|
||||||
public string Status { get; init; }
|
public string Status { get; init; }
|
||||||
public string TrackedDownloadStatus { get; init; }
|
public string TrackedDownloadStatus { get; init; }
|
||||||
@@ -13,5 +23,5 @@ public sealed record QueueRecord
|
|||||||
public List<TrackedDownloadStatusMessage>? StatusMessages { get; init; }
|
public List<TrackedDownloadStatusMessage>? StatusMessages { get; init; }
|
||||||
public required string DownloadId { get; init; }
|
public required string DownloadId { get; init; }
|
||||||
public required string Protocol { get; init; }
|
public required string Protocol { get; init; }
|
||||||
public required int Id { get; init; }
|
public required long Id { get; init; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Domain.Models.Lidarr;
|
||||||
|
|
||||||
|
public sealed record Album
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
public long ArtistId { get; set; }
|
||||||
|
|
||||||
|
public Artist Artist { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Domain.Models.Lidarr;
|
||||||
|
|
||||||
|
public sealed record Artist
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public string ArtistName { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Domain.Models.Lidarr;
|
||||||
|
|
||||||
|
public sealed record LidarrCommand
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public List<long> AlbumIds { get; set; }
|
||||||
|
|
||||||
|
public long ArtistId { get; set; }
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
using Common.Configuration;
|
using Common.Configuration.Arr;
|
||||||
using Common.Configuration.Arr;
|
|
||||||
using Common.Configuration.ContentBlocker;
|
using Common.Configuration.ContentBlocker;
|
||||||
using Common.Configuration.DownloadClient;
|
using Common.Configuration.DownloadClient;
|
||||||
using Common.Configuration.Logging;
|
using Common.Configuration.Logging;
|
||||||
using Common.Configuration.QueueCleaner;
|
using Common.Configuration.QueueCleaner;
|
||||||
using Domain.Enums;
|
|
||||||
|
|
||||||
namespace Executable.DependencyInjection;
|
namespace Executable.DependencyInjection;
|
||||||
|
|
||||||
@@ -20,5 +18,6 @@ public static class ConfigurationDI
|
|||||||
.Configure<TransmissionConfig>(configuration.GetSection(TransmissionConfig.SectionName))
|
.Configure<TransmissionConfig>(configuration.GetSection(TransmissionConfig.SectionName))
|
||||||
.Configure<SonarrConfig>(configuration.GetSection(SonarrConfig.SectionName))
|
.Configure<SonarrConfig>(configuration.GetSection(SonarrConfig.SectionName))
|
||||||
.Configure<RadarrConfig>(configuration.GetSection(RadarrConfig.SectionName))
|
.Configure<RadarrConfig>(configuration.GetSection(RadarrConfig.SectionName))
|
||||||
|
.Configure<LidarrConfig>(configuration.GetSection(LidarrConfig.SectionName))
|
||||||
.Configure<LoggingConfig>(configuration.GetSection(LoggingConfig.SectionName));
|
.Configure<LoggingConfig>(configuration.GetSection(LoggingConfig.SectionName));
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Common.Configuration.Logging;
|
using Common.Configuration.Logging;
|
||||||
|
using Domain.Enums;
|
||||||
using Infrastructure.Verticals.ContentBlocker;
|
using Infrastructure.Verticals.ContentBlocker;
|
||||||
using Infrastructure.Verticals.QueueCleaner;
|
using Infrastructure.Verticals.QueueCleaner;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
@@ -27,11 +28,22 @@ public static class LoggingDI
|
|||||||
}
|
}
|
||||||
|
|
||||||
LoggerConfiguration logConfig = new();
|
LoggerConfiguration logConfig = new();
|
||||||
const string consoleOutputTemplate = "[{@t:yyyy-MM-dd HH:mm:ss.fff} {@l:u3}]{#if JobName is not null} {Concat('[',JobName,']'),PAD}{#end} {@m}\n{@x}";
|
const string jobNameTemplate = "{#if JobName is not null} {Concat('[',JobName,']'),JOB_PAD}{#end}";
|
||||||
const string fileOutputTemplate = "{@t:yyyy-MM-dd HH:mm:ss.fff zzz} [{@l:u3}]{#if JobName is not null} {Concat('[',JobName,']'),PAD}{#end} {@m:lj}\n{@x}";
|
const string instanceNameTemplate = "{#if InstanceName is not null} {Concat('[',InstanceName,']'),ARR_PAD}{#end}";
|
||||||
|
const string consoleOutputTemplate = $"[{{@t:yyyy-MM-dd HH:mm:ss.fff}} {{@l:u3}}]{jobNameTemplate}{instanceNameTemplate} {{@m}}\n{{@x}}";
|
||||||
|
const string fileOutputTemplate = $"{{@t:yyyy-MM-dd HH:mm:ss.fff zzz}} [{{@l:u3}}]{jobNameTemplate}{instanceNameTemplate} {{@m:lj}}\n{{@x}}";
|
||||||
LogEventLevel level = LogEventLevel.Information;
|
LogEventLevel level = LogEventLevel.Information;
|
||||||
List<string> jobNames = [nameof(ContentBlocker), nameof(QueueCleaner)];
|
List<string> names = [nameof(ContentBlocker), nameof(QueueCleaner)];
|
||||||
int padding = jobNames.Max(x => x.Length) + 2;
|
int jobPadding = names.Max(x => x.Length) + 2;
|
||||||
|
names = [InstanceType.Sonarr.ToString(), InstanceType.Radarr.ToString(), InstanceType.Lidarr.ToString()];
|
||||||
|
int arrPadding = names.Max(x => x.Length) + 2;
|
||||||
|
|
||||||
|
string consoleTemplate = consoleOutputTemplate
|
||||||
|
.Replace("JOB_PAD", jobPadding.ToString())
|
||||||
|
.Replace("ARR_PAD", arrPadding.ToString());
|
||||||
|
string fileTemplate = fileOutputTemplate
|
||||||
|
.Replace("JOB_PAD", jobPadding.ToString())
|
||||||
|
.Replace("ARR_PAD", arrPadding.ToString());
|
||||||
|
|
||||||
if (config is not null)
|
if (config is not null)
|
||||||
{
|
{
|
||||||
@@ -41,7 +53,7 @@ public static class LoggingDI
|
|||||||
{
|
{
|
||||||
logConfig.WriteTo.File(
|
logConfig.WriteTo.File(
|
||||||
path: Path.Combine(config.File.Path, "cleanuperr-.txt"),
|
path: Path.Combine(config.File.Path, "cleanuperr-.txt"),
|
||||||
formatter: new ExpressionTemplate(fileOutputTemplate.Replace("PAD", padding.ToString())),
|
formatter: new ExpressionTemplate(fileTemplate),
|
||||||
fileSizeLimitBytes: 10L * 1024 * 1024,
|
fileSizeLimitBytes: 10L * 1024 * 1024,
|
||||||
rollingInterval: RollingInterval.Day,
|
rollingInterval: RollingInterval.Day,
|
||||||
rollOnFileSizeLimit: true
|
rollOnFileSizeLimit: true
|
||||||
@@ -55,7 +67,7 @@ public static class LoggingDI
|
|||||||
.MinimumLevel.Override("Microsoft.Extensions.Http", LogEventLevel.Warning)
|
.MinimumLevel.Override("Microsoft.Extensions.Http", LogEventLevel.Warning)
|
||||||
.MinimumLevel.Override("Quartz", LogEventLevel.Warning)
|
.MinimumLevel.Override("Quartz", LogEventLevel.Warning)
|
||||||
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Error)
|
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Error)
|
||||||
.WriteTo.Console(new ExpressionTemplate(consoleOutputTemplate.Replace("PAD", padding.ToString())))
|
.WriteTo.Console(new ExpressionTemplate(consoleTemplate))
|
||||||
.Enrich.FromLogContext()
|
.Enrich.FromLogContext()
|
||||||
.Enrich.WithProperty("ApplicationName", "cleanuperr")
|
.Enrich.WithProperty("ApplicationName", "cleanuperr")
|
||||||
.CreateLogger();
|
.CreateLogger();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public static class ServicesDI
|
|||||||
services
|
services
|
||||||
.AddTransient<SonarrClient>()
|
.AddTransient<SonarrClient>()
|
||||||
.AddTransient<RadarrClient>()
|
.AddTransient<RadarrClient>()
|
||||||
|
.AddTransient<LidarrClient>()
|
||||||
.AddTransient<QueueCleaner>()
|
.AddTransient<QueueCleaner>()
|
||||||
.AddTransient<ContentBlocker>()
|
.AddTransient<ContentBlocker>()
|
||||||
.AddTransient<FilenameEvaluator>()
|
.AddTransient<FilenameEvaluator>()
|
||||||
|
|||||||
@@ -16,25 +16,20 @@
|
|||||||
"ContentBlocker": {
|
"ContentBlocker": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"IGNORE_PRIVATE": true,
|
"IGNORE_PRIVATE": true,
|
||||||
"Blacklist": {
|
"DELETE_PRIVATE": false
|
||||||
"Enabled": false,
|
|
||||||
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
|
|
||||||
},
|
|
||||||
"Whitelist": {
|
|
||||||
"Enabled": false,
|
|
||||||
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/whitelist"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"QueueCleaner": {
|
"QueueCleaner": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"RunSequentially": true,
|
"RunSequentially": true,
|
||||||
"IMPORT_FAILED_MAX_STRIKES": 5,
|
"IMPORT_FAILED_MAX_STRIKES": 5,
|
||||||
"IMPORT_FAILED_IGNORE_PRIVATE": true,
|
"IMPORT_FAILED_IGNORE_PRIVATE": true,
|
||||||
|
"IMPORT_FAILED_DELETE_PRIVATE": false,
|
||||||
"IMPORT_FAILED_IGNORE_PATTERNS": [
|
"IMPORT_FAILED_IGNORE_PATTERNS": [
|
||||||
"file is a sample"
|
"file is a sample"
|
||||||
],
|
],
|
||||||
"STALLED_MAX_STRIKES": 5,
|
"STALLED_MAX_STRIKES": 5,
|
||||||
"STALLED_IGNORE_PRIVATE": true
|
"STALLED_IGNORE_PRIVATE": true,
|
||||||
|
"STALLED_DELETE_PRIVATE": false
|
||||||
},
|
},
|
||||||
"DOWNLOAD_CLIENT": "qbittorrent",
|
"DOWNLOAD_CLIENT": "qbittorrent",
|
||||||
"qBittorrent": {
|
"qBittorrent": {
|
||||||
@@ -54,19 +49,40 @@
|
|||||||
"Sonarr": {
|
"Sonarr": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"SearchType": "Episode",
|
"SearchType": "Episode",
|
||||||
|
"Block": {
|
||||||
|
"Type": "blacklist",
|
||||||
|
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
|
||||||
|
},
|
||||||
"Instances": [
|
"Instances": [
|
||||||
{
|
{
|
||||||
"Url": "http://localhost:8989",
|
"Url": "http://localhost:8989",
|
||||||
"ApiKey": "96736c3eb3144936b8f1d62d27be8cee"
|
"ApiKey": "425d1e713f0c405cbbf359ac0502c1f4"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Radarr": {
|
"Radarr": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
|
"Block": {
|
||||||
|
"Type": "blacklist",
|
||||||
|
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
|
||||||
|
},
|
||||||
"Instances": [
|
"Instances": [
|
||||||
{
|
{
|
||||||
"Url": "http://localhost:7878",
|
"Url": "http://localhost:7878",
|
||||||
"ApiKey": "705b553732ab4167ab23909305d60600"
|
"ApiKey": "8b7454f668e54c5b8f44f56f93969761"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Lidarr": {
|
||||||
|
"Enabled": true,
|
||||||
|
"Block": {
|
||||||
|
"Type": "blacklist",
|
||||||
|
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
|
||||||
|
},
|
||||||
|
"Instances": [
|
||||||
|
{
|
||||||
|
"Url": "http://localhost:8686",
|
||||||
|
"ApiKey": "7f677cfdc074414397af53dd633860c5"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,26 +15,20 @@
|
|||||||
},
|
},
|
||||||
"ContentBlocker": {
|
"ContentBlocker": {
|
||||||
"Enabled": false,
|
"Enabled": false,
|
||||||
"IGNORE_PRIVATE": false,
|
"IGNORE_PRIVATE": false
|
||||||
"Blacklist": {
|
|
||||||
"Enabled": false,
|
|
||||||
"Path": ""
|
|
||||||
},
|
|
||||||
"Whitelist": {
|
|
||||||
"Enabled": false,
|
|
||||||
"Path": ""
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"QueueCleaner": {
|
"QueueCleaner": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"RunSequentially": true,
|
"RunSequentially": true,
|
||||||
"IMPORT_FAILED_MAX_STRIKES": 5,
|
"IMPORT_FAILED_MAX_STRIKES": 5,
|
||||||
"IMPORT_FAILED_IGNORE_PRIVATE": false,
|
"IMPORT_FAILED_IGNORE_PRIVATE": false,
|
||||||
|
"IMPORT_FAILED_DELETE_PRIVATE": false,
|
||||||
"IMPORT_FAILED_IGNORE_PATTERNS": [],
|
"IMPORT_FAILED_IGNORE_PATTERNS": [],
|
||||||
"STALLED_MAX_STRIKES": 5,
|
"STALLED_MAX_STRIKES": 5,
|
||||||
"STALLED_IGNORE_PRIVATE": false
|
"STALLED_IGNORE_PRIVATE": false,
|
||||||
|
"STALLED_DELETE_PRIVATE": false
|
||||||
},
|
},
|
||||||
"DOWNLOAD_CLIENT": "qbittorrent",
|
"DOWNLOAD_CLIENT": "none",
|
||||||
"qBittorrent": {
|
"qBittorrent": {
|
||||||
"Url": "http://localhost:8080",
|
"Url": "http://localhost:8080",
|
||||||
"Username": "",
|
"Username": "",
|
||||||
@@ -50,8 +44,12 @@
|
|||||||
"Password": "testing"
|
"Password": "testing"
|
||||||
},
|
},
|
||||||
"Sonarr": {
|
"Sonarr": {
|
||||||
"Enabled": true,
|
"Enabled": false,
|
||||||
"SearchType": "Episode",
|
"SearchType": "Episode",
|
||||||
|
"Block": {
|
||||||
|
"Type": "blacklist",
|
||||||
|
"Path": ""
|
||||||
|
},
|
||||||
"Instances": [
|
"Instances": [
|
||||||
{
|
{
|
||||||
"Url": "http://localhost:8989",
|
"Url": "http://localhost:8989",
|
||||||
@@ -61,11 +59,28 @@
|
|||||||
},
|
},
|
||||||
"Radarr": {
|
"Radarr": {
|
||||||
"Enabled": false,
|
"Enabled": false,
|
||||||
|
"Block": {
|
||||||
|
"Type": "blacklist",
|
||||||
|
"Path": ""
|
||||||
|
},
|
||||||
"Instances": [
|
"Instances": [
|
||||||
{
|
{
|
||||||
"Url": "http://localhost:7878",
|
"Url": "http://localhost:7878",
|
||||||
"ApiKey": ""
|
"ApiKey": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"Lidarr": {
|
||||||
|
"Enabled": false,
|
||||||
|
"Block": {
|
||||||
|
"Type": "blacklist",
|
||||||
|
"Path": ""
|
||||||
|
},
|
||||||
|
"Instances": [
|
||||||
|
{
|
||||||
|
"Url": "http://localhost:8686",
|
||||||
|
"ApiKey": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,9 +101,9 @@ public abstract class ArrClient
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord queueRecord)
|
public virtual async Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record, bool removeFromClient)
|
||||||
{
|
{
|
||||||
Uri uri = new(arrInstance.Url, $"/api/v3/queue/{queueRecord.Id}?removeFromClient=true&blocklist=true&skipRedownload=true&changeCategory=false");
|
Uri uri = new(arrInstance.Url, GetQueueDeleteUrlPath(record.Id, removeFromClient));
|
||||||
|
|
||||||
using HttpRequestMessage request = new(HttpMethod.Delete, uri);
|
using HttpRequestMessage request = new(HttpMethod.Delete, uri);
|
||||||
SetApiKey(request, arrInstance.ApiKey);
|
SetApiKey(request, arrInstance.ApiKey);
|
||||||
@@ -113,17 +113,23 @@ public abstract class ArrClient
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
_logger.LogInformation("queue item deleted | {url} | {title}", arrInstance.Url, queueRecord.Title);
|
_logger.LogInformation(
|
||||||
|
removeFromClient
|
||||||
|
? "queue item deleted | {url} | {title}"
|
||||||
|
: "queue item removed from arr | {url} | {title}",
|
||||||
|
arrInstance.Url,
|
||||||
|
record.Title
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
_logger.LogError("queue delete failed | {uri} | {title}", uri, queueRecord.Title);
|
_logger.LogError("queue delete failed | {uri} | {title}", uri, record.Title);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Task RefreshItemsAsync(ArrInstance arrInstance, ArrConfig config, HashSet<SearchItem>? items);
|
public abstract Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items);
|
||||||
|
|
||||||
public virtual bool IsRecordValid(QueueRecord record)
|
public virtual bool IsRecordValid(QueueRecord record)
|
||||||
{
|
{
|
||||||
@@ -143,6 +149,8 @@ public abstract class ArrClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected abstract string GetQueueUrlPath(int page);
|
protected abstract string GetQueueUrlPath(int page);
|
||||||
|
|
||||||
|
protected abstract string GetQueueDeleteUrlPath(long recordId, bool removeFromClient);
|
||||||
|
|
||||||
protected virtual void SetApiKey(HttpRequestMessage request, string apiKey)
|
protected virtual void SetApiKey(HttpRequestMessage request, string apiKey)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Common.Configuration.Arr;
|
||||||
|
using Common.Configuration.Logging;
|
||||||
|
using Common.Configuration.QueueCleaner;
|
||||||
|
using Domain.Models.Arr;
|
||||||
|
using Domain.Models.Arr.Queue;
|
||||||
|
using Domain.Models.Lidarr;
|
||||||
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Infrastructure.Verticals.Arr;
|
||||||
|
|
||||||
|
public sealed class LidarrClient : ArrClient
|
||||||
|
{
|
||||||
|
public LidarrClient(
|
||||||
|
ILogger<LidarrClient> logger,
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
IOptions<LoggingConfig> loggingConfig,
|
||||||
|
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
||||||
|
Striker striker
|
||||||
|
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GetQueueUrlPath(int page)
|
||||||
|
{
|
||||||
|
return $"/api/v1/queue?page={page}&pageSize=200&includeUnknownArtistItems=true&includeArtist=true&includeAlbum=true";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GetQueueDeleteUrlPath(long recordId, bool removeFromClient)
|
||||||
|
{
|
||||||
|
string path = $"/api/v1/queue/{recordId}?blocklist=true&skipRedownload=true&changeCategory=false";
|
||||||
|
|
||||||
|
path += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
||||||
|
{
|
||||||
|
if (items?.Count is null or 0) return;
|
||||||
|
|
||||||
|
Uri uri = new(arrInstance.Url, "/api/v1/command");
|
||||||
|
|
||||||
|
foreach (var command in GetSearchCommands(items))
|
||||||
|
{
|
||||||
|
using HttpRequestMessage request = new(HttpMethod.Post, uri);
|
||||||
|
request.Content = new StringContent(
|
||||||
|
JsonConvert.SerializeObject(command, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
|
||||||
|
Encoding.UTF8,
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
SetApiKey(request, arrInstance.ApiKey);
|
||||||
|
|
||||||
|
using var response = await _httpClient.SendAsync(request);
|
||||||
|
string? logContext = await ComputeCommandLogContextAsync(arrInstance, command);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
_logger.LogInformation("{log}", GetSearchLog(arrInstance.Url, command, true, logContext));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_logger.LogError("{log}", GetSearchLog(arrInstance.Url, command, false, logContext));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool IsRecordValid(QueueRecord record)
|
||||||
|
{
|
||||||
|
if (record.ArtistId is 0 || record.AlbumId is 0)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("skip | artist id and/or album id missing | {title}", record.Title);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.IsRecordValid(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetSearchLog(
|
||||||
|
Uri instanceUrl,
|
||||||
|
LidarrCommand command,
|
||||||
|
bool success,
|
||||||
|
string? logContext
|
||||||
|
)
|
||||||
|
{
|
||||||
|
string status = success ? "triggered" : "failed";
|
||||||
|
|
||||||
|
return $"album search {status} | {instanceUrl} | {logContext ?? $"albums: {string.Join(',', command.AlbumIds)}"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> ComputeCommandLogContextAsync(ArrInstance arrInstance, LidarrCommand command)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_loggingConfig.Enhanced) return null;
|
||||||
|
|
||||||
|
StringBuilder log = new();
|
||||||
|
|
||||||
|
var albums = await GetAlbumsAsync(arrInstance, command.AlbumIds);
|
||||||
|
|
||||||
|
if (albums?.Count is null or 0) return null;
|
||||||
|
|
||||||
|
var groups = albums
|
||||||
|
.GroupBy(x => x.Artist.Id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
var first = group.First();
|
||||||
|
|
||||||
|
log.Append($"[{first.Artist.ArtistName} albums {string.Join(',', group.Select(x => x.Title).ToList())}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return log.ToString();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(exception, "failed to compute log context");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<Album>?> GetAlbumsAsync(ArrInstance arrInstance, List<long> albumIds)
|
||||||
|
{
|
||||||
|
Uri uri = new(arrInstance.Url, $"api/v1/album?{string.Join('&', albumIds.Select(x => $"albumIds={x}"))}");
|
||||||
|
using HttpRequestMessage request = new(HttpMethod.Get, uri);
|
||||||
|
SetApiKey(request, arrInstance.ApiKey);
|
||||||
|
|
||||||
|
using var response = await _httpClient.SendAsync(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
string responseBody = await response.Content.ReadAsStringAsync();
|
||||||
|
return JsonConvert.DeserializeObject<List<Album>>(responseBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<LidarrCommand> GetSearchCommands(HashSet<SearchItem> items)
|
||||||
|
{
|
||||||
|
const string albumSearch = "AlbumSearch";
|
||||||
|
|
||||||
|
return [new LidarrCommand { Name = albumSearch, AlbumIds = items.Select(i => i.Id).ToList() }];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ using Domain.Models.Arr;
|
|||||||
using Domain.Models.Arr.Queue;
|
using Domain.Models.Arr.Queue;
|
||||||
using Domain.Models.Radarr;
|
using Domain.Models.Radarr;
|
||||||
using Infrastructure.Verticals.ItemStriker;
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -30,7 +29,16 @@ public sealed class RadarrClient : ArrClient
|
|||||||
return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownMovieItems=true&includeMovie=true";
|
return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownMovieItems=true&includeMovie=true";
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task RefreshItemsAsync(ArrInstance arrInstance, ArrConfig config, HashSet<SearchItem>? items)
|
protected override string GetQueueDeleteUrlPath(long recordId, bool removeFromClient)
|
||||||
|
{
|
||||||
|
string path = $"/api/v3/queue/{recordId}?blocklist=true&skipRedownload=true&changeCategory=false";
|
||||||
|
|
||||||
|
path += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
||||||
{
|
{
|
||||||
if (items?.Count is null or 0)
|
if (items?.Count is null or 0)
|
||||||
{
|
{
|
||||||
@@ -74,7 +82,7 @@ public sealed class RadarrClient : ArrClient
|
|||||||
{
|
{
|
||||||
if (record.MovieId is 0)
|
if (record.MovieId is 0)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("skip | item information missing | {title}", record.Title);
|
_logger.LogDebug("skip | movie id missing | {title}", record.Title);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using Domain.Models.Arr;
|
|||||||
using Domain.Models.Arr.Queue;
|
using Domain.Models.Arr.Queue;
|
||||||
using Domain.Models.Sonarr;
|
using Domain.Models.Sonarr;
|
||||||
using Infrastructure.Verticals.ItemStriker;
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -29,8 +28,17 @@ public sealed class SonarrClient : ArrClient
|
|||||||
{
|
{
|
||||||
return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownSeriesItems=true&includeSeries=true";
|
return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownSeriesItems=true&includeSeries=true";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override string GetQueueDeleteUrlPath(long recordId, bool removeFromClient)
|
||||||
|
{
|
||||||
|
string path = $"/api/v3/queue/{recordId}?blocklist=true&skipRedownload=true&changeCategory=false";
|
||||||
|
|
||||||
public override async Task RefreshItemsAsync(ArrInstance arrInstance, ArrConfig config, HashSet<SearchItem>? items)
|
path += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
||||||
{
|
{
|
||||||
if (items?.Count is null or 0)
|
if (items?.Count is null or 0)
|
||||||
{
|
{
|
||||||
@@ -70,7 +78,7 @@ public sealed class SonarrClient : ArrClient
|
|||||||
{
|
{
|
||||||
if (record.EpisodeId is 0 || record.SeriesId is 0)
|
if (record.EpisodeId is 0 || record.SeriesId is 0)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("skip | item information missing | {title}", record.Title);
|
_logger.LogDebug("skip | episode id and/or series id missing | {title}", record.Title);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Common.Configuration.Arr;
|
||||||
using Common.Configuration.ContentBlocker;
|
using Common.Configuration.ContentBlocker;
|
||||||
using Common.Helpers;
|
using Common.Helpers;
|
||||||
using Domain.Enums;
|
using Domain.Enums;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
@@ -12,78 +14,98 @@ namespace Infrastructure.Verticals.ContentBlocker;
|
|||||||
public sealed class BlocklistProvider
|
public sealed class BlocklistProvider
|
||||||
{
|
{
|
||||||
private readonly ILogger<BlocklistProvider> _logger;
|
private readonly ILogger<BlocklistProvider> _logger;
|
||||||
private readonly ContentBlockerConfig _config;
|
private readonly SonarrConfig _sonarrConfig;
|
||||||
|
private readonly RadarrConfig _radarrConfig;
|
||||||
|
private readonly LidarrConfig _lidarrConfig;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
public BlocklistType BlocklistType { get; }
|
private bool _initialized;
|
||||||
|
|
||||||
public ConcurrentBag<string> Patterns { get; } = [];
|
private const string Type = "type";
|
||||||
|
private const string Patterns = "patterns";
|
||||||
public ConcurrentBag<Regex> Regexes { get; } = [];
|
private const string Regexes = "regexes";
|
||||||
|
|
||||||
public BlocklistProvider(
|
public BlocklistProvider(
|
||||||
ILogger<BlocklistProvider> logger,
|
ILogger<BlocklistProvider> logger,
|
||||||
IOptions<ContentBlockerConfig> config,
|
IOptions<SonarrConfig> sonarrConfig,
|
||||||
IHttpClientFactory httpClientFactory)
|
IOptions<RadarrConfig> radarrConfig,
|
||||||
|
IOptions<LidarrConfig> lidarrConfig,
|
||||||
|
IMemoryCache cache,
|
||||||
|
IHttpClientFactory httpClientFactory
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_config = config.Value;
|
_sonarrConfig = sonarrConfig.Value;
|
||||||
|
_radarrConfig = radarrConfig.Value;
|
||||||
|
_lidarrConfig = lidarrConfig.Value;
|
||||||
|
_cache = cache;
|
||||||
_httpClient = httpClientFactory.CreateClient(Constants.HttpClientWithRetryName);
|
_httpClient = httpClientFactory.CreateClient(Constants.HttpClientWithRetryName);
|
||||||
|
|
||||||
_config.Validate();
|
|
||||||
|
|
||||||
if (_config.Blacklist?.Enabled is true)
|
|
||||||
{
|
|
||||||
BlocklistType = BlocklistType.Blacklist;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_config.Whitelist?.Enabled is true)
|
|
||||||
{
|
|
||||||
BlocklistType = BlocklistType.Whitelist;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadBlocklistAsync()
|
public async Task LoadBlocklistsAsync()
|
||||||
{
|
{
|
||||||
if (Patterns.Count > 0 || Regexes.Count > 0)
|
if (_initialized)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("blocklist already loaded");
|
_logger.LogDebug("blocklists already loaded");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await LoadPatternsAndRegexesAsync();
|
await LoadPatternsAndRegexesAsync(_sonarrConfig.Block.Type, _sonarrConfig.Block.Path, InstanceType.Sonarr);
|
||||||
|
await LoadPatternsAndRegexesAsync(_radarrConfig.Block.Type, _radarrConfig.Block.Path, InstanceType.Radarr);
|
||||||
|
await LoadPatternsAndRegexesAsync(_lidarrConfig.Block.Type, _lidarrConfig.Block.Path, InstanceType.Lidarr);
|
||||||
|
|
||||||
|
_initialized = true;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
_logger.LogError("failed to load {type}", BlocklistType.ToString());
|
_logger.LogError("failed to load blocklists");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadPatternsAndRegexesAsync()
|
public BlocklistType GetBlocklistType(InstanceType instanceType)
|
||||||
{
|
{
|
||||||
string[] patterns;
|
_cache.TryGetValue($"{instanceType.ToString()}_{Type}", out BlocklistType? blocklistType);
|
||||||
|
|
||||||
|
return blocklistType ?? BlocklistType.Blacklist;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConcurrentBag<string> GetPatterns(InstanceType instanceType)
|
||||||
|
{
|
||||||
|
_cache.TryGetValue($"{instanceType.ToString()}_{Patterns}", out ConcurrentBag<string>? patterns);
|
||||||
|
|
||||||
|
return patterns ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConcurrentBag<Regex> GetRegexes(InstanceType instanceType)
|
||||||
|
{
|
||||||
|
_cache.TryGetValue($"{instanceType.ToString()}_{Regexes}", out ConcurrentBag<Regex>? regexes);
|
||||||
|
|
||||||
if (BlocklistType is BlocklistType.Blacklist)
|
return regexes ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadPatternsAndRegexesAsync(BlocklistType blocklistType, string? blocklistPath, InstanceType instanceType)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(blocklistPath))
|
||||||
{
|
{
|
||||||
patterns = await ReadContentAsync(_config.Blacklist.Path);
|
return;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
patterns = await ReadContentAsync(_config.Whitelist.Path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string[] filePatterns = await ReadContentAsync(blocklistPath);
|
||||||
|
|
||||||
long startTime = Stopwatch.GetTimestamp();
|
long startTime = Stopwatch.GetTimestamp();
|
||||||
ParallelOptions options = new() { MaxDegreeOfParallelism = 5 };
|
ParallelOptions options = new() { MaxDegreeOfParallelism = 5 };
|
||||||
const string regexId = "regex:";
|
const string regexId = "regex:";
|
||||||
|
ConcurrentBag<string> patterns = [];
|
||||||
|
ConcurrentBag<Regex> regexes = [];
|
||||||
|
|
||||||
Parallel.ForEach(patterns, options, pattern =>
|
Parallel.ForEach(filePatterns, options, pattern =>
|
||||||
{
|
{
|
||||||
if (!pattern.StartsWith(regexId))
|
if (!pattern.StartsWith(regexId))
|
||||||
{
|
{
|
||||||
Patterns.Add(pattern);
|
patterns.Add(pattern);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +114,7 @@ public sealed class BlocklistProvider
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Regex regex = new(pattern, RegexOptions.Compiled);
|
Regex regex = new(pattern, RegexOptions.Compiled);
|
||||||
Regexes.Add(regex);
|
regexes.Add(regex);
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
@@ -101,10 +123,14 @@ public sealed class BlocklistProvider
|
|||||||
});
|
});
|
||||||
|
|
||||||
TimeSpan elapsed = Stopwatch.GetElapsedTime(startTime);
|
TimeSpan elapsed = Stopwatch.GetElapsedTime(startTime);
|
||||||
|
|
||||||
|
_cache.Set($"{instanceType.ToString()}_{Type}", blocklistType);
|
||||||
|
_cache.Set($"{instanceType.ToString()}_{Patterns}", patterns);
|
||||||
|
_cache.Set($"{instanceType.ToString()}_{Regexes}", regexes);
|
||||||
|
|
||||||
_logger.LogDebug("loaded {count} patterns", Patterns.Count);
|
_logger.LogDebug("loaded {count} patterns", patterns.Count);
|
||||||
_logger.LogDebug("loaded {count} regexes", Regexes.Count);
|
_logger.LogDebug("loaded {count} regexes", regexes.Count);
|
||||||
_logger.LogDebug("blocklist loaded in {elapsed} ms", elapsed.TotalMilliseconds);
|
_logger.LogDebug("blocklist loaded in {elapsed} ms | {path}", elapsed.TotalMilliseconds, blocklistPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string[]> ReadContentAsync(string path)
|
private async Task<string[]> ReadContentAsync(string path)
|
||||||
|
|||||||
@@ -1,31 +1,46 @@
|
|||||||
using Common.Configuration.Arr;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Common.Configuration.Arr;
|
||||||
|
using Common.Configuration.ContentBlocker;
|
||||||
using Common.Configuration.DownloadClient;
|
using Common.Configuration.DownloadClient;
|
||||||
using Domain.Enums;
|
using Domain.Enums;
|
||||||
|
using Domain.Models.Arr;
|
||||||
using Domain.Models.Arr.Queue;
|
using Domain.Models.Arr.Queue;
|
||||||
using Infrastructure.Verticals.Arr;
|
using Infrastructure.Verticals.Arr;
|
||||||
using Infrastructure.Verticals.DownloadClient;
|
using Infrastructure.Verticals.DownloadClient;
|
||||||
using Infrastructure.Verticals.Jobs;
|
using Infrastructure.Verticals.Jobs;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Serilog.Context;
|
||||||
|
|
||||||
namespace Infrastructure.Verticals.ContentBlocker;
|
namespace Infrastructure.Verticals.ContentBlocker;
|
||||||
|
|
||||||
public sealed class ContentBlocker : GenericHandler
|
public sealed class ContentBlocker : GenericHandler
|
||||||
{
|
{
|
||||||
|
private readonly ContentBlockerConfig _config;
|
||||||
private readonly BlocklistProvider _blocklistProvider;
|
private readonly BlocklistProvider _blocklistProvider;
|
||||||
|
|
||||||
public ContentBlocker(
|
public ContentBlocker(
|
||||||
ILogger<ContentBlocker> logger,
|
ILogger<ContentBlocker> logger,
|
||||||
|
IOptions<ContentBlockerConfig> config,
|
||||||
IOptions<DownloadClientConfig> downloadClientConfig,
|
IOptions<DownloadClientConfig> downloadClientConfig,
|
||||||
IOptions<SonarrConfig> sonarrConfig,
|
IOptions<SonarrConfig> sonarrConfig,
|
||||||
IOptions<RadarrConfig> radarrConfig,
|
IOptions<RadarrConfig> radarrConfig,
|
||||||
|
IOptions<LidarrConfig> lidarrConfig,
|
||||||
SonarrClient sonarrClient,
|
SonarrClient sonarrClient,
|
||||||
RadarrClient radarrClient,
|
RadarrClient radarrClient,
|
||||||
|
LidarrClient lidarrClient,
|
||||||
ArrQueueIterator arrArrQueueIterator,
|
ArrQueueIterator arrArrQueueIterator,
|
||||||
BlocklistProvider blocklistProvider,
|
BlocklistProvider blocklistProvider,
|
||||||
DownloadServiceFactory downloadServiceFactory
|
DownloadServiceFactory downloadServiceFactory
|
||||||
) : base(logger, downloadClientConfig, sonarrConfig.Value, radarrConfig.Value, sonarrClient, radarrClient, arrArrQueueIterator, downloadServiceFactory)
|
) : base(
|
||||||
|
logger, downloadClientConfig,
|
||||||
|
sonarrConfig, radarrConfig, lidarrConfig,
|
||||||
|
sonarrClient, radarrClient, lidarrClient,
|
||||||
|
arrArrQueueIterator, downloadServiceFactory
|
||||||
|
)
|
||||||
{
|
{
|
||||||
|
_config = config.Value;
|
||||||
_blocklistProvider = blocklistProvider;
|
_blocklistProvider = blocklistProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,18 +52,40 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _blocklistProvider.LoadBlocklistAsync();
|
bool blocklistIsConfigured = _sonarrConfig.Enabled && !string.IsNullOrEmpty(_sonarrConfig.Block.Path) ||
|
||||||
|
_radarrConfig.Enabled && !string.IsNullOrEmpty(_radarrConfig.Block.Path) ||
|
||||||
|
_lidarrConfig.Enabled && !string.IsNullOrEmpty(_lidarrConfig.Block.Path);
|
||||||
|
|
||||||
|
if (!blocklistIsConfigured)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("no blocklist is configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _blocklistProvider.LoadBlocklistsAsync();
|
||||||
await base.ExecuteAsync();
|
await base.ExecuteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||||
{
|
{
|
||||||
|
using var _ = LogContext.PushProperty("InstanceName", instanceType.ToString());
|
||||||
|
|
||||||
|
HashSet<SearchItem> itemsToBeRefreshed = [];
|
||||||
ArrClient arrClient = GetClient(instanceType);
|
ArrClient arrClient = GetClient(instanceType);
|
||||||
|
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||||
|
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||||
|
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||||
|
|
||||||
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
|
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
|
||||||
{
|
{
|
||||||
foreach (QueueRecord record in items)
|
var groups = items
|
||||||
|
.GroupBy(x => x.DownloadId)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var group in groups)
|
||||||
{
|
{
|
||||||
|
QueueRecord record = group.First();
|
||||||
|
|
||||||
if (record.Protocol is not "torrent")
|
if (record.Protocol is not "torrent")
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -61,8 +98,30 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("searching unwanted files for {title}", record.Title);
|
_logger.LogDebug("searching unwanted files for {title}", record.Title);
|
||||||
await _downloadService.BlockUnwantedFilesAsync(record.DownloadId);
|
|
||||||
|
BlockFilesResult result = await _downloadService
|
||||||
|
.BlockUnwantedFilesAsync(record.DownloadId, blocklistType, patterns, regexes);
|
||||||
|
|
||||||
|
if (!result.ShouldRemove)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("all files are marked as unwanted | {hash}", record.Title);
|
||||||
|
|
||||||
|
itemsToBeRefreshed.Add(GetRecordSearchItem(instanceType, record, group.Count() > 1));
|
||||||
|
|
||||||
|
bool removeFromClient = true;
|
||||||
|
|
||||||
|
if (result.IsPrivate && !_config.DeletePrivate)
|
||||||
|
{
|
||||||
|
removeFromClient = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await arrClient.RefreshItemsAsync(instance, itemsToBeRefreshed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using Domain.Enums;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Common.Configuration.ContentBlocker;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Infrastructure.Verticals.ContentBlocker;
|
namespace Infrastructure.Verticals.ContentBlocker;
|
||||||
@@ -6,46 +8,44 @@ namespace Infrastructure.Verticals.ContentBlocker;
|
|||||||
public sealed class FilenameEvaluator
|
public sealed class FilenameEvaluator
|
||||||
{
|
{
|
||||||
private readonly ILogger<FilenameEvaluator> _logger;
|
private readonly ILogger<FilenameEvaluator> _logger;
|
||||||
private readonly BlocklistProvider _blocklistProvider;
|
|
||||||
|
|
||||||
public FilenameEvaluator(ILogger<FilenameEvaluator> logger, BlocklistProvider blocklistProvider)
|
public FilenameEvaluator(ILogger<FilenameEvaluator> logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_blocklistProvider = blocklistProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO create unit tests
|
// TODO create unit tests
|
||||||
public bool IsValid(string filename)
|
public bool IsValid(string filename, BlocklistType type, ConcurrentBag<string> patterns, ConcurrentBag<Regex> regexes)
|
||||||
{
|
{
|
||||||
return IsValidAgainstPatterns(filename) && IsValidAgainstRegexes(filename);
|
return IsValidAgainstPatterns(filename, type, patterns) && IsValidAgainstRegexes(filename, type, regexes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsValidAgainstPatterns(string filename)
|
private static bool IsValidAgainstPatterns(string filename, BlocklistType type, ConcurrentBag<string> patterns)
|
||||||
{
|
{
|
||||||
if (_blocklistProvider.Patterns.Count is 0)
|
if (patterns.Count is 0)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _blocklistProvider.BlocklistType switch
|
return type switch
|
||||||
{
|
{
|
||||||
BlocklistType.Blacklist => !_blocklistProvider.Patterns.Any(pattern => MatchesPattern(filename, pattern)),
|
BlocklistType.Blacklist => !patterns.Any(pattern => MatchesPattern(filename, pattern)),
|
||||||
BlocklistType.Whitelist => _blocklistProvider.Patterns.Any(pattern => MatchesPattern(filename, pattern)),
|
BlocklistType.Whitelist => patterns.Any(pattern => MatchesPattern(filename, pattern)),
|
||||||
_ => true
|
_ => true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsValidAgainstRegexes(string filename)
|
private static bool IsValidAgainstRegexes(string filename, BlocklistType type, ConcurrentBag<Regex> regexes)
|
||||||
{
|
{
|
||||||
if (_blocklistProvider.Regexes.Count is 0)
|
if (regexes.Count is 0)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _blocklistProvider.BlocklistType switch
|
return type switch
|
||||||
{
|
{
|
||||||
BlocklistType.Blacklist => !_blocklistProvider.Regexes.Any(regex => regex.IsMatch(filename)),
|
BlocklistType.Blacklist => !regexes.Any(regex => regex.IsMatch(filename)),
|
||||||
BlocklistType.Whitelist => _blocklistProvider.Regexes.Any(regex => regex.IsMatch(filename)),
|
BlocklistType.Whitelist => regexes.Any(regex => regex.IsMatch(filename)),
|
||||||
_ => true
|
_ => true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Infrastructure.Verticals.DownloadClient;
|
||||||
|
|
||||||
|
public sealed record BlockFilesResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// True if the download should be removed; otherwise false.
|
||||||
|
/// </summary>
|
||||||
|
public bool ShouldRemove { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the download is private; otherwise false.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsPrivate { get; set; }
|
||||||
|
}
|
||||||
@@ -78,6 +78,11 @@ public sealed class DelugeClient
|
|||||||
await SendRequest<DelugeResponse<object>>("core.set_torrent_options", hash, filePriorities);
|
await SendRequest<DelugeResponse<object>>("core.set_torrent_options", hash, filePriorities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DelugeResponse<object>> DeleteTorrent(string hash)
|
||||||
|
{
|
||||||
|
return await SendRequest<DelugeResponse<object>>("core.remove_torrents", new List<string> { hash }, true);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<String> PostJson(String json)
|
private async Task<String> PostJson(String json)
|
||||||
{
|
{
|
||||||
StringContent content = new StringContent(json);
|
StringContent content = new StringContent(json);
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Common.Configuration.ContentBlocker;
|
||||||
using Common.Configuration.DownloadClient;
|
using Common.Configuration.DownloadClient;
|
||||||
using Common.Configuration.QueueCleaner;
|
using Common.Configuration.QueueCleaner;
|
||||||
using Domain.Models.Deluge.Response;
|
using Domain.Models.Deluge.Response;
|
||||||
@@ -17,9 +20,10 @@ public sealed class DelugeService : DownloadServiceBase
|
|||||||
IOptions<DelugeConfig> config,
|
IOptions<DelugeConfig> config,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
||||||
|
IOptions<ContentBlockerConfig> contentBlockerConfig,
|
||||||
FilenameEvaluator filenameEvaluator,
|
FilenameEvaluator filenameEvaluator,
|
||||||
Striker striker
|
Striker striker
|
||||||
) : base(logger, queueCleanerConfig, filenameEvaluator, striker)
|
) : base(logger, queueCleanerConfig, contentBlockerConfig, filenameEvaluator, striker)
|
||||||
{
|
{
|
||||||
config.Value.Validate();
|
config.Value.Validate();
|
||||||
_client = new (config, httpClientFactory);
|
_client = new (config, httpClientFactory);
|
||||||
@@ -30,12 +34,13 @@ public sealed class DelugeService : DownloadServiceBase
|
|||||||
await _client.LoginAsync();
|
await _client.LoginAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash)
|
/// <inheritdoc/>
|
||||||
|
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
|
||||||
{
|
{
|
||||||
hash = hash.ToLowerInvariant();
|
hash = hash.ToLowerInvariant();
|
||||||
|
|
||||||
DelugeContents? contents = null;
|
DelugeContents? contents = null;
|
||||||
RemoveResult result = new();
|
StalledResult result = new();
|
||||||
|
|
||||||
TorrentStatus? status = await GetTorrentStatus(hash);
|
TorrentStatus? status = await GetTorrentStatus(hash);
|
||||||
|
|
||||||
@@ -70,23 +75,32 @@ public sealed class DelugeService : DownloadServiceBase
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task BlockUnwantedFilesAsync(string hash)
|
/// <inheritdoc/>
|
||||||
|
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
||||||
|
string hash,
|
||||||
|
BlocklistType blocklistType,
|
||||||
|
ConcurrentBag<string> patterns,
|
||||||
|
ConcurrentBag<Regex> regexes
|
||||||
|
)
|
||||||
{
|
{
|
||||||
hash = hash.ToLowerInvariant();
|
hash = hash.ToLowerInvariant();
|
||||||
|
|
||||||
TorrentStatus? status = await GetTorrentStatus(hash);
|
TorrentStatus? status = await GetTorrentStatus(hash);
|
||||||
|
BlockFilesResult result = new();
|
||||||
|
|
||||||
if (status?.Hash is null)
|
if (status?.Hash is null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||||
return;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.IsPrivate = status.Private;
|
||||||
|
|
||||||
if (_queueCleanerConfig.StalledIgnorePrivate && status.Private)
|
if (_contentBlockerConfig.IgnorePrivate && status.Private)
|
||||||
{
|
{
|
||||||
// ignore private trackers
|
// ignore private trackers
|
||||||
_logger.LogDebug("skip files check | download is private | {name}", status.Name);
|
_logger.LogDebug("skip files check | download is private | {name}", status.Name);
|
||||||
return;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
DelugeContents? contents = null;
|
DelugeContents? contents = null;
|
||||||
@@ -102,18 +116,27 @@ public sealed class DelugeService : DownloadServiceBase
|
|||||||
|
|
||||||
if (contents is null)
|
if (contents is null)
|
||||||
{
|
{
|
||||||
return;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Dictionary<int, int> priorities = [];
|
Dictionary<int, int> priorities = [];
|
||||||
bool hasPriorityUpdates = false;
|
bool hasPriorityUpdates = false;
|
||||||
|
long totalFiles = 0;
|
||||||
|
long totalUnwantedFiles = 0;
|
||||||
|
|
||||||
ProcessFiles(contents.Contents, (name, file) =>
|
ProcessFiles(contents.Contents, (name, file) =>
|
||||||
{
|
{
|
||||||
|
totalFiles++;
|
||||||
int priority = file.Priority;
|
int priority = file.Priority;
|
||||||
|
|
||||||
if (file.Priority is not 0 && !_filenameEvaluator.IsValid(name))
|
if (file.Priority is 0)
|
||||||
{
|
{
|
||||||
|
totalUnwantedFiles++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.Priority is not 0 && !_filenameEvaluator.IsValid(name, blocklistType, patterns, regexes))
|
||||||
|
{
|
||||||
|
totalUnwantedFiles++;
|
||||||
priority = 0;
|
priority = 0;
|
||||||
hasPriorityUpdates = true;
|
hasPriorityUpdates = true;
|
||||||
_logger.LogInformation("unwanted file found | {file}", file.Path);
|
_logger.LogInformation("unwanted file found | {file}", file.Path);
|
||||||
@@ -124,7 +147,7 @@ public sealed class DelugeService : DownloadServiceBase
|
|||||||
|
|
||||||
if (!hasPriorityUpdates)
|
if (!hasPriorityUpdates)
|
||||||
{
|
{
|
||||||
return;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("changing priorities | torrent {hash}", hash);
|
_logger.LogDebug("changing priorities | torrent {hash}", hash);
|
||||||
@@ -134,7 +157,25 @@ public sealed class DelugeService : DownloadServiceBase
|
|||||||
.Select(x => x.Value)
|
.Select(x => x.Value)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
if (totalUnwantedFiles == totalFiles)
|
||||||
|
{
|
||||||
|
// Skip marking files as unwanted. The download will be removed completely.
|
||||||
|
result.ShouldRemove = true;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
await _client.ChangeFilesPriority(hash, sortedPriorities);
|
await _client.ChangeFilesPriority(hash, sortedPriorities);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override async Task Delete(string hash)
|
||||||
|
{
|
||||||
|
hash = hash.ToLowerInvariant();
|
||||||
|
|
||||||
|
await _client.DeleteTorrent(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsItemStuckAndShouldRemove(TorrentStatus status)
|
private bool IsItemStuckAndShouldRemove(TorrentStatus status)
|
||||||
@@ -173,8 +214,13 @@ public sealed class DelugeService : DownloadServiceBase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ProcessFiles(Dictionary<string, DelugeFileOrDirectory> contents, Action<string, DelugeFileOrDirectory> processFile)
|
private static void ProcessFiles(Dictionary<string, DelugeFileOrDirectory>? contents, Action<string, DelugeFileOrDirectory> processFile)
|
||||||
{
|
{
|
||||||
|
if (contents is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var (name, data) in contents)
|
foreach (var (name, data) in contents)
|
||||||
{
|
{
|
||||||
switch (data.Type)
|
switch (data.Type)
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Common.Configuration.QueueCleaner;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Common.Configuration.ContentBlocker;
|
||||||
|
using Common.Configuration.QueueCleaner;
|
||||||
using Domain.Enums;
|
using Domain.Enums;
|
||||||
using Infrastructure.Verticals.ContentBlocker;
|
using Infrastructure.Verticals.ContentBlocker;
|
||||||
using Infrastructure.Verticals.ItemStriker;
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
@@ -11,18 +14,21 @@ public abstract class DownloadServiceBase : IDownloadService
|
|||||||
{
|
{
|
||||||
protected readonly ILogger<DownloadServiceBase> _logger;
|
protected readonly ILogger<DownloadServiceBase> _logger;
|
||||||
protected readonly QueueCleanerConfig _queueCleanerConfig;
|
protected readonly QueueCleanerConfig _queueCleanerConfig;
|
||||||
|
protected readonly ContentBlockerConfig _contentBlockerConfig;
|
||||||
protected readonly FilenameEvaluator _filenameEvaluator;
|
protected readonly FilenameEvaluator _filenameEvaluator;
|
||||||
protected readonly Striker _striker;
|
protected readonly Striker _striker;
|
||||||
|
|
||||||
protected DownloadServiceBase(
|
protected DownloadServiceBase(
|
||||||
ILogger<DownloadServiceBase> logger,
|
ILogger<DownloadServiceBase> logger,
|
||||||
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
||||||
|
IOptions<ContentBlockerConfig> contentBlockerConfig,
|
||||||
FilenameEvaluator filenameEvaluator,
|
FilenameEvaluator filenameEvaluator,
|
||||||
Striker striker
|
Striker striker
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_queueCleanerConfig = queueCleanerConfig.Value;
|
_queueCleanerConfig = queueCleanerConfig.Value;
|
||||||
|
_contentBlockerConfig = contentBlockerConfig.Value;
|
||||||
_filenameEvaluator = filenameEvaluator;
|
_filenameEvaluator = filenameEvaluator;
|
||||||
_striker = striker;
|
_striker = striker;
|
||||||
}
|
}
|
||||||
@@ -31,10 +37,25 @@ public abstract class DownloadServiceBase : IDownloadService
|
|||||||
|
|
||||||
public abstract Task LoginAsync();
|
public abstract Task LoginAsync();
|
||||||
|
|
||||||
public abstract Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash);
|
public abstract Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash);
|
||||||
|
|
||||||
public abstract Task BlockUnwantedFilesAsync(string hash);
|
/// <inheritdoc/>
|
||||||
|
public abstract Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
||||||
|
string hash,
|
||||||
|
BlocklistType blocklistType,
|
||||||
|
ConcurrentBag<string> patterns,
|
||||||
|
ConcurrentBag<Regex> regexes
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public abstract Task Delete(string hash);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Strikes an item and checks if the limit has been reached.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hash">The torrent hash.</param>
|
||||||
|
/// <param name="itemName">The name or title of the item.</param>
|
||||||
|
/// <returns>True if the limit has been reached; otherwise, false.</returns>
|
||||||
protected bool StrikeAndCheckLimit(string hash, string itemName)
|
protected bool StrikeAndCheckLimit(string hash, string itemName)
|
||||||
{
|
{
|
||||||
return _striker.StrikeAndCheckLimit(hash, itemName, _queueCleanerConfig.StalledMaxStrikes, StrikeType.Stalled);
|
return _striker.StrikeAndCheckLimit(hash, itemName, _queueCleanerConfig.StalledMaxStrikes, StrikeType.Stalled);
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Common.Configuration.QueueCleaner;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Common.Configuration.ContentBlocker;
|
||||||
|
using Common.Configuration.QueueCleaner;
|
||||||
using Infrastructure.Verticals.ContentBlocker;
|
using Infrastructure.Verticals.ContentBlocker;
|
||||||
using Infrastructure.Verticals.ItemStriker;
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -8,7 +11,7 @@ namespace Infrastructure.Verticals.DownloadClient;
|
|||||||
|
|
||||||
public sealed class DummyDownloadService : DownloadServiceBase
|
public sealed class DummyDownloadService : DownloadServiceBase
|
||||||
{
|
{
|
||||||
public DummyDownloadService(ILogger<DownloadServiceBase> logger, IOptions<QueueCleanerConfig> queueCleanerConfig, FilenameEvaluator filenameEvaluator, Striker striker) : base(logger, queueCleanerConfig, filenameEvaluator, striker)
|
public DummyDownloadService(ILogger<DownloadServiceBase> logger, IOptions<QueueCleanerConfig> queueCleanerConfig, IOptions<ContentBlockerConfig> contentBlockerConfig, FilenameEvaluator filenameEvaluator, Striker striker) : base(logger, queueCleanerConfig, contentBlockerConfig, filenameEvaluator, striker)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,12 +24,17 @@ public sealed class DummyDownloadService : DownloadServiceBase
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash)
|
public override Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task BlockUnwantedFilesAsync(string hash)
|
public override Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash, BlocklistType blocklistType, ConcurrentBag<string> patterns, ConcurrentBag<Regex> regexes)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task Delete(string hash)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,36 @@
|
|||||||
namespace Infrastructure.Verticals.DownloadClient;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Common.Configuration.ContentBlocker;
|
||||||
|
|
||||||
|
namespace Infrastructure.Verticals.DownloadClient;
|
||||||
|
|
||||||
public interface IDownloadService : IDisposable
|
public interface IDownloadService : IDisposable
|
||||||
{
|
{
|
||||||
public Task LoginAsync();
|
public Task LoginAsync();
|
||||||
|
|
||||||
public Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash);
|
/// <summary>
|
||||||
|
/// Checks whether the download should be removed from the *arr queue.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hash">The download hash.</param>
|
||||||
|
public Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash);
|
||||||
|
|
||||||
public Task BlockUnwantedFilesAsync(string hash);
|
/// <summary>
|
||||||
|
/// Blocks unwanted files from being fully downloaded.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hash">The torrent hash.</param>
|
||||||
|
/// <param name="blocklistType">The <see cref="BlocklistType"/>.</param>
|
||||||
|
/// <param name="patterns">The patterns to test the files against.</param>
|
||||||
|
/// <param name="regexes">The regexes to test the files against.</param>
|
||||||
|
/// <returns>True if all files have been blocked; otherwise false.</returns>
|
||||||
|
public Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
||||||
|
string hash,
|
||||||
|
BlocklistType blocklistType,
|
||||||
|
ConcurrentBag<string> patterns,
|
||||||
|
ConcurrentBag<Regex> regexes
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a download item.
|
||||||
|
/// </summary>
|
||||||
|
public Task Delete(string hash);
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Common.Configuration.ContentBlocker;
|
||||||
using Common.Configuration.DownloadClient;
|
using Common.Configuration.DownloadClient;
|
||||||
using Common.Configuration.QueueCleaner;
|
using Common.Configuration.QueueCleaner;
|
||||||
using Common.Helpers;
|
using Common.Helpers;
|
||||||
@@ -19,9 +22,10 @@ public sealed class QBitService : DownloadServiceBase
|
|||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IOptions<QBitConfig> config,
|
IOptions<QBitConfig> config,
|
||||||
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
||||||
|
IOptions<ContentBlockerConfig> contentBlockerConfig,
|
||||||
FilenameEvaluator filenameEvaluator,
|
FilenameEvaluator filenameEvaluator,
|
||||||
Striker striker
|
Striker striker
|
||||||
) : base(logger, queueCleanerConfig, filenameEvaluator, striker)
|
) : base(logger, queueCleanerConfig, contentBlockerConfig, filenameEvaluator, striker)
|
||||||
{
|
{
|
||||||
_config = config.Value;
|
_config = config.Value;
|
||||||
_config.Validate();
|
_config.Validate();
|
||||||
@@ -38,9 +42,10 @@ public sealed class QBitService : DownloadServiceBase
|
|||||||
await _client.LoginAsync(_config.Username, _config.Password);
|
await _client.LoginAsync(_config.Username, _config.Password);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash)
|
/// <inheritdoc/>
|
||||||
|
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
|
||||||
{
|
{
|
||||||
RemoveResult result = new();
|
StalledResult result = new();
|
||||||
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
@@ -83,15 +88,22 @@ public sealed class QBitService : DownloadServiceBase
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task BlockUnwantedFilesAsync(string hash)
|
/// <inheritdoc/>
|
||||||
|
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
||||||
|
string hash,
|
||||||
|
BlocklistType blocklistType,
|
||||||
|
ConcurrentBag<string> patterns,
|
||||||
|
ConcurrentBag<Regex> regexes
|
||||||
|
)
|
||||||
{
|
{
|
||||||
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
BlockFilesResult result = new();
|
||||||
|
|
||||||
if (torrent is null)
|
if (torrent is null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||||
return;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash);
|
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash);
|
||||||
@@ -99,26 +111,32 @@ public sealed class QBitService : DownloadServiceBase
|
|||||||
if (torrentProperties is null)
|
if (torrentProperties is null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("failed to find torrent properties {hash} in the download client", hash);
|
_logger.LogDebug("failed to find torrent properties {hash} in the download client", hash);
|
||||||
return;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isPrivate = torrentProperties.AdditionalData.TryGetValue("is_private", out var dictValue) &&
|
bool isPrivate = torrentProperties.AdditionalData.TryGetValue("is_private", out var dictValue) &&
|
||||||
bool.TryParse(dictValue?.ToString(), out bool boolValue)
|
bool.TryParse(dictValue?.ToString(), out bool boolValue)
|
||||||
&& boolValue;
|
&& boolValue;
|
||||||
|
|
||||||
if (_queueCleanerConfig.StalledIgnorePrivate && isPrivate)
|
result.IsPrivate = isPrivate;
|
||||||
|
|
||||||
|
if (_contentBlockerConfig.IgnorePrivate && isPrivate)
|
||||||
{
|
{
|
||||||
// ignore private trackers
|
// ignore private trackers
|
||||||
_logger.LogDebug("skip files check | download is private | {name}", torrent.Name);
|
_logger.LogDebug("skip files check | download is private | {name}", torrent.Name);
|
||||||
return;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
|
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
|
||||||
|
|
||||||
if (files is null)
|
if (files is null)
|
||||||
{
|
{
|
||||||
return;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<int> unwantedFiles = [];
|
||||||
|
long totalFiles = 0;
|
||||||
|
long totalUnwantedFiles = 0;
|
||||||
|
|
||||||
foreach (TorrentContent file in files)
|
foreach (TorrentContent file in files)
|
||||||
{
|
{
|
||||||
@@ -127,14 +145,49 @@ public sealed class QBitService : DownloadServiceBase
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.Priority is TorrentContentPriority.Skip || _filenameEvaluator.IsValid(file.Name))
|
totalFiles++;
|
||||||
|
|
||||||
|
if (file.Priority is TorrentContentPriority.Skip)
|
||||||
|
{
|
||||||
|
totalUnwantedFiles++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_filenameEvaluator.IsValid(file.Name, blocklistType, patterns, regexes))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("unwanted file found | {file}", file.Name);
|
_logger.LogInformation("unwanted file found | {file}", file.Name);
|
||||||
await _client.SetFilePriorityAsync(hash, file.Index.Value, TorrentContentPriority.Skip);
|
unwantedFiles.Add(file.Index.Value);
|
||||||
|
totalUnwantedFiles++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (unwantedFiles.Count is 0)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalUnwantedFiles == totalFiles)
|
||||||
|
{
|
||||||
|
// Skip marking files as unwanted. The download will be removed completely.
|
||||||
|
result.ShouldRemove = true;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (int fileIndex in unwantedFiles)
|
||||||
|
{
|
||||||
|
await _client.SetFilePriorityAsync(hash, fileIndex, TorrentContentPriority.Skip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override async Task Delete(string hash)
|
||||||
|
{
|
||||||
|
await _client.DeleteAsync(hash, deleteDownloadedData: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
namespace Infrastructure.Verticals.DownloadClient;
|
namespace Infrastructure.Verticals.DownloadClient;
|
||||||
|
|
||||||
public sealed record RemoveResult
|
public sealed record StalledResult
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True if the download should be removed; otherwise false.
|
/// True if the download should be removed; otherwise false.
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
using Common.Configuration.DownloadClient;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Common.Configuration.ContentBlocker;
|
||||||
|
using Common.Configuration.DownloadClient;
|
||||||
using Common.Configuration.QueueCleaner;
|
using Common.Configuration.QueueCleaner;
|
||||||
using Common.Helpers;
|
using Common.Helpers;
|
||||||
using Infrastructure.Verticals.ContentBlocker;
|
using Infrastructure.Verticals.ContentBlocker;
|
||||||
@@ -22,9 +25,10 @@ public sealed class TransmissionService : DownloadServiceBase
|
|||||||
ILogger<TransmissionService> logger,
|
ILogger<TransmissionService> logger,
|
||||||
IOptions<TransmissionConfig> config,
|
IOptions<TransmissionConfig> config,
|
||||||
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
||||||
|
IOptions<ContentBlockerConfig> contentBlockerConfig,
|
||||||
FilenameEvaluator filenameEvaluator,
|
FilenameEvaluator filenameEvaluator,
|
||||||
Striker striker
|
Striker striker
|
||||||
) : base(logger, queueCleanerConfig, filenameEvaluator, striker)
|
) : base(logger, queueCleanerConfig, contentBlockerConfig, filenameEvaluator, striker)
|
||||||
{
|
{
|
||||||
_config = config.Value;
|
_config = config.Value;
|
||||||
_config.Validate();
|
_config.Validate();
|
||||||
@@ -41,9 +45,10 @@ public sealed class TransmissionService : DownloadServiceBase
|
|||||||
await _client.GetSessionInformationAsync();
|
await _client.GetSessionInformationAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash)
|
/// <inheritdoc/>
|
||||||
|
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
|
||||||
{
|
{
|
||||||
RemoveResult result = new();
|
StalledResult result = new();
|
||||||
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
||||||
|
|
||||||
if (torrent is null)
|
if (torrent is null)
|
||||||
@@ -76,23 +81,35 @@ public sealed class TransmissionService : DownloadServiceBase
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task BlockUnwantedFilesAsync(string hash)
|
/// <inheritdoc/>
|
||||||
|
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
||||||
|
string hash,
|
||||||
|
BlocklistType blocklistType,
|
||||||
|
ConcurrentBag<string> patterns,
|
||||||
|
ConcurrentBag<Regex> regexes
|
||||||
|
)
|
||||||
{
|
{
|
||||||
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
||||||
|
BlockFilesResult result = new();
|
||||||
|
|
||||||
if (torrent?.FileStats is null || torrent.Files is null)
|
if (torrent?.FileStats is null || torrent.Files is null)
|
||||||
{
|
{
|
||||||
return;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isPrivate = torrent.IsPrivate ?? false;
|
||||||
|
result.IsPrivate = isPrivate;
|
||||||
|
|
||||||
if (_queueCleanerConfig.StalledIgnorePrivate && (torrent.IsPrivate ?? false))
|
if (_contentBlockerConfig.IgnorePrivate && isPrivate)
|
||||||
{
|
{
|
||||||
// ignore private trackers
|
// ignore private trackers
|
||||||
_logger.LogDebug("skip files check | download is private | {name}", torrent.Name);
|
_logger.LogDebug("skip files check | download is private | {name}", torrent.Name);
|
||||||
return;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<long> unwantedFiles = [];
|
List<long> unwantedFiles = [];
|
||||||
|
long totalFiles = 0;
|
||||||
|
long totalUnwantedFiles = 0;
|
||||||
|
|
||||||
for (int i = 0; i < torrent.Files.Length; i++)
|
for (int i = 0; i < torrent.Files.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -100,19 +117,36 @@ public sealed class TransmissionService : DownloadServiceBase
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalFiles++;
|
||||||
|
|
||||||
if (!torrent.FileStats[i].Wanted.Value || _filenameEvaluator.IsValid(torrent.Files[i].Name))
|
if (!torrent.FileStats[i].Wanted.Value)
|
||||||
|
{
|
||||||
|
totalUnwantedFiles++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_filenameEvaluator.IsValid(torrent.Files[i].Name, blocklistType, patterns, regexes))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("unwanted file found | {file}", torrent.Files[i].Name);
|
_logger.LogInformation("unwanted file found | {file}", torrent.Files[i].Name);
|
||||||
unwantedFiles.Add(i);
|
unwantedFiles.Add(i);
|
||||||
|
totalUnwantedFiles++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unwantedFiles.Count is 0)
|
if (unwantedFiles.Count is 0)
|
||||||
{
|
{
|
||||||
return;
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalUnwantedFiles == totalFiles)
|
||||||
|
{
|
||||||
|
// Skip marking files as unwanted. The download will be removed completely.
|
||||||
|
result.ShouldRemove = true;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("changing priorities | torrent {hash}", hash);
|
_logger.LogDebug("changing priorities | torrent {hash}", hash);
|
||||||
@@ -122,6 +156,20 @@ public sealed class TransmissionService : DownloadServiceBase
|
|||||||
Ids = [ torrent.Id ],
|
Ids = [ torrent.Id ],
|
||||||
FilesUnwanted = unwantedFiles.ToArray(),
|
FilesUnwanted = unwantedFiles.ToArray(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task Delete(string hash)
|
||||||
|
{
|
||||||
|
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
||||||
|
|
||||||
|
if (torrent is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _client.TorrentRemoveAsync([torrent.Id], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public class Striker
|
|||||||
++strikeCount;
|
++strikeCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("item on strike number {strike} | reason {reason} | {name}", strikeCount, strikeType.ToString(), itemName);
|
_logger.LogInformation("item on strike number {strike} | reason {reason} | {name}", strikeCount, strikeType.ToString(), itemName);
|
||||||
_cache.Set(key, strikeCount, _cacheOptions);
|
_cache.Set(key, strikeCount, _cacheOptions);
|
||||||
|
|
||||||
if (strikeCount < maxStrikes)
|
if (strikeCount < maxStrikes)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Common.Configuration.Arr;
|
using Common.Configuration.Arr;
|
||||||
using Common.Configuration.DownloadClient;
|
using Common.Configuration.DownloadClient;
|
||||||
using Domain.Enums;
|
using Domain.Enums;
|
||||||
using Domain.Models.Arr;
|
using Domain.Models.Arr;
|
||||||
@@ -16,28 +16,34 @@ public abstract class GenericHandler : IDisposable
|
|||||||
protected readonly DownloadClientConfig _downloadClientConfig;
|
protected readonly DownloadClientConfig _downloadClientConfig;
|
||||||
protected readonly SonarrConfig _sonarrConfig;
|
protected readonly SonarrConfig _sonarrConfig;
|
||||||
protected readonly RadarrConfig _radarrConfig;
|
protected readonly RadarrConfig _radarrConfig;
|
||||||
|
protected readonly LidarrConfig _lidarrConfig;
|
||||||
protected readonly SonarrClient _sonarrClient;
|
protected readonly SonarrClient _sonarrClient;
|
||||||
protected readonly RadarrClient _radarrClient;
|
protected readonly RadarrClient _radarrClient;
|
||||||
|
protected readonly LidarrClient _lidarrClient;
|
||||||
protected readonly ArrQueueIterator _arrArrQueueIterator;
|
protected readonly ArrQueueIterator _arrArrQueueIterator;
|
||||||
protected readonly IDownloadService _downloadService;
|
protected readonly IDownloadService _downloadService;
|
||||||
|
|
||||||
protected GenericHandler(
|
protected GenericHandler(
|
||||||
ILogger<GenericHandler> logger,
|
ILogger<GenericHandler> logger,
|
||||||
IOptions<DownloadClientConfig> downloadClientConfig,
|
IOptions<DownloadClientConfig> downloadClientConfig,
|
||||||
SonarrConfig sonarrConfig,
|
IOptions<SonarrConfig> sonarrConfig,
|
||||||
RadarrConfig radarrConfig,
|
IOptions<RadarrConfig> radarrConfig,
|
||||||
|
IOptions<LidarrConfig> lidarrConfig,
|
||||||
SonarrClient sonarrClient,
|
SonarrClient sonarrClient,
|
||||||
RadarrClient radarrClient,
|
RadarrClient radarrClient,
|
||||||
|
LidarrClient lidarrClient,
|
||||||
ArrQueueIterator arrArrQueueIterator,
|
ArrQueueIterator arrArrQueueIterator,
|
||||||
DownloadServiceFactory downloadServiceFactory
|
DownloadServiceFactory downloadServiceFactory
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_downloadClientConfig = downloadClientConfig.Value;
|
_downloadClientConfig = downloadClientConfig.Value;
|
||||||
_sonarrConfig = sonarrConfig;
|
_sonarrConfig = sonarrConfig.Value;
|
||||||
_radarrConfig = radarrConfig;
|
_radarrConfig = radarrConfig.Value;
|
||||||
|
_lidarrConfig = lidarrConfig.Value;
|
||||||
_sonarrClient = sonarrClient;
|
_sonarrClient = sonarrClient;
|
||||||
_radarrClient = radarrClient;
|
_radarrClient = radarrClient;
|
||||||
|
_lidarrClient = lidarrClient;
|
||||||
_arrArrQueueIterator = arrArrQueueIterator;
|
_arrArrQueueIterator = arrArrQueueIterator;
|
||||||
_downloadService = downloadServiceFactory.CreateDownloadClient();
|
_downloadService = downloadServiceFactory.CreateDownloadClient();
|
||||||
}
|
}
|
||||||
@@ -48,6 +54,7 @@ public abstract class GenericHandler : IDisposable
|
|||||||
|
|
||||||
await ProcessArrConfigAsync(_sonarrConfig, InstanceType.Sonarr);
|
await ProcessArrConfigAsync(_sonarrConfig, InstanceType.Sonarr);
|
||||||
await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr);
|
await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr);
|
||||||
|
await ProcessArrConfigAsync(_lidarrConfig, InstanceType.Lidarr);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Dispose()
|
public virtual void Dispose()
|
||||||
@@ -82,17 +89,10 @@ public abstract class GenericHandler : IDisposable
|
|||||||
{
|
{
|
||||||
InstanceType.Sonarr => _sonarrClient,
|
InstanceType.Sonarr => _sonarrClient,
|
||||||
InstanceType.Radarr => _radarrClient,
|
InstanceType.Radarr => _radarrClient,
|
||||||
|
InstanceType.Lidarr => _lidarrClient,
|
||||||
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
||||||
};
|
};
|
||||||
|
|
||||||
protected ArrConfig GetConfig(InstanceType type) =>
|
|
||||||
type switch
|
|
||||||
{
|
|
||||||
InstanceType.Sonarr => _sonarrConfig,
|
|
||||||
InstanceType.Radarr => _radarrConfig,
|
|
||||||
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
|
||||||
};
|
|
||||||
|
|
||||||
protected SearchItem GetRecordSearchItem(InstanceType type, QueueRecord record, bool isPack = false)
|
protected SearchItem GetRecordSearchItem(InstanceType type, QueueRecord record, bool isPack = false)
|
||||||
{
|
{
|
||||||
return type switch
|
return type switch
|
||||||
@@ -117,11 +117,15 @@ public abstract class GenericHandler : IDisposable
|
|||||||
},
|
},
|
||||||
InstanceType.Sonarr when _sonarrConfig.SearchType is SonarrSearchType.Series => new SonarrSearchItem
|
InstanceType.Sonarr when _sonarrConfig.SearchType is SonarrSearchType.Series => new SonarrSearchItem
|
||||||
{
|
{
|
||||||
Id = record.SeriesId,
|
Id = record.SeriesId
|
||||||
},
|
},
|
||||||
InstanceType.Radarr => new SearchItem
|
InstanceType.Radarr => new SearchItem
|
||||||
{
|
{
|
||||||
Id = record.MovieId,
|
Id = record.MovieId
|
||||||
|
},
|
||||||
|
InstanceType.Lidarr => new SearchItem
|
||||||
|
{
|
||||||
|
Id = record.AlbumId
|
||||||
},
|
},
|
||||||
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Common.Configuration.Arr;
|
using Common.Configuration.Arr;
|
||||||
using Common.Configuration.DownloadClient;
|
using Common.Configuration.DownloadClient;
|
||||||
|
using Common.Configuration.QueueCleaner;
|
||||||
using Domain.Enums;
|
using Domain.Enums;
|
||||||
using Domain.Models.Arr;
|
using Domain.Models.Arr;
|
||||||
using Domain.Models.Arr.Queue;
|
using Domain.Models.Arr.Queue;
|
||||||
@@ -8,29 +9,42 @@ using Infrastructure.Verticals.DownloadClient;
|
|||||||
using Infrastructure.Verticals.Jobs;
|
using Infrastructure.Verticals.Jobs;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Serilog.Context;
|
||||||
|
|
||||||
namespace Infrastructure.Verticals.QueueCleaner;
|
namespace Infrastructure.Verticals.QueueCleaner;
|
||||||
|
|
||||||
public sealed class QueueCleaner : GenericHandler
|
public sealed class QueueCleaner : GenericHandler
|
||||||
{
|
{
|
||||||
|
private readonly QueueCleanerConfig _config;
|
||||||
|
|
||||||
public QueueCleaner(
|
public QueueCleaner(
|
||||||
ILogger<QueueCleaner> logger,
|
ILogger<QueueCleaner> logger,
|
||||||
|
IOptions<QueueCleanerConfig> config,
|
||||||
IOptions<DownloadClientConfig> downloadClientConfig,
|
IOptions<DownloadClientConfig> downloadClientConfig,
|
||||||
IOptions<SonarrConfig> sonarrConfig,
|
IOptions<SonarrConfig> sonarrConfig,
|
||||||
IOptions<RadarrConfig> radarrConfig,
|
IOptions<RadarrConfig> radarrConfig,
|
||||||
|
IOptions<LidarrConfig> lidarrConfig,
|
||||||
SonarrClient sonarrClient,
|
SonarrClient sonarrClient,
|
||||||
RadarrClient radarrClient,
|
RadarrClient radarrClient,
|
||||||
|
LidarrClient lidarrClient,
|
||||||
ArrQueueIterator arrArrQueueIterator,
|
ArrQueueIterator arrArrQueueIterator,
|
||||||
DownloadServiceFactory downloadServiceFactory
|
DownloadServiceFactory downloadServiceFactory
|
||||||
) : base(logger, downloadClientConfig, sonarrConfig.Value, radarrConfig.Value, sonarrClient, radarrClient, arrArrQueueIterator, downloadServiceFactory)
|
) : base(
|
||||||
|
logger, downloadClientConfig,
|
||||||
|
sonarrConfig, radarrConfig, lidarrConfig,
|
||||||
|
sonarrClient, radarrClient, lidarrClient,
|
||||||
|
arrArrQueueIterator, downloadServiceFactory
|
||||||
|
)
|
||||||
{
|
{
|
||||||
|
_config = config.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||||
{
|
{
|
||||||
|
using var _ = LogContext.PushProperty("InstanceName", instanceType.ToString());
|
||||||
|
|
||||||
HashSet<SearchItem> itemsToBeRefreshed = [];
|
HashSet<SearchItem> itemsToBeRefreshed = [];
|
||||||
ArrClient arrClient = GetClient(instanceType);
|
ArrClient arrClient = GetClient(instanceType);
|
||||||
ArrConfig arrConfig = GetConfig(instanceType);
|
|
||||||
|
|
||||||
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
|
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
|
||||||
{
|
{
|
||||||
@@ -57,16 +71,18 @@ public sealed class QueueCleaner : GenericHandler
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveResult removeResult = new();
|
StalledResult stalledCheckResult = new();
|
||||||
|
|
||||||
if (_downloadClientConfig.DownloadClient is not Common.Enums.DownloadClient.None)
|
if (_downloadClientConfig.DownloadClient is not Common.Enums.DownloadClient.None)
|
||||||
{
|
{
|
||||||
removeResult = await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId);
|
// stalled download check
|
||||||
|
stalledCheckResult = await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool shouldRemoveFromArr = arrClient.ShouldRemoveFromQueue(record, removeResult.IsPrivate);
|
// failed import check
|
||||||
|
bool shouldRemoveFromArr = arrClient.ShouldRemoveFromQueue(record, stalledCheckResult.IsPrivate);
|
||||||
|
|
||||||
if (!shouldRemoveFromArr && !removeResult.ShouldRemove)
|
if (!shouldRemoveFromArr && !stalledCheckResult.ShouldRemove)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("skip | {title}", record.Title);
|
_logger.LogInformation("skip | {title}", record.Title);
|
||||||
continue;
|
continue;
|
||||||
@@ -74,10 +90,25 @@ public sealed class QueueCleaner : GenericHandler
|
|||||||
|
|
||||||
itemsToBeRefreshed.Add(GetRecordSearchItem(instanceType, record, group.Count() > 1));
|
itemsToBeRefreshed.Add(GetRecordSearchItem(instanceType, record, group.Count() > 1));
|
||||||
|
|
||||||
await arrClient.DeleteQueueItemAsync(instance, record);
|
bool removeFromClient = true;
|
||||||
|
|
||||||
|
if (stalledCheckResult.IsPrivate)
|
||||||
|
{
|
||||||
|
if (stalledCheckResult.ShouldRemove && !_config.StalledDeletePrivate)
|
||||||
|
{
|
||||||
|
removeFromClient = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRemoveFromArr && !_config.ImportFailedDeletePrivate)
|
||||||
|
{
|
||||||
|
removeFromClient = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await arrClient.RefreshItemsAsync(instance, arrConfig, itemsToBeRefreshed);
|
await arrClient.RefreshItemsAsync(instance, itemsToBeRefreshed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
*.mp3
|
||||||
@@ -3,6 +3,24 @@
|
|||||||
"format": 1
|
"format": 1
|
||||||
}{
|
}{
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"lidarr": {
|
||||||
|
"apply_max": false,
|
||||||
|
"apply_move_completed": false,
|
||||||
|
"apply_queue": false,
|
||||||
|
"auto_add": false,
|
||||||
|
"auto_add_trackers": [],
|
||||||
|
"is_auto_managed": false,
|
||||||
|
"max_connections": -1,
|
||||||
|
"max_download_speed": -1,
|
||||||
|
"max_upload_slots": -1,
|
||||||
|
"max_upload_speed": -1,
|
||||||
|
"move_completed": false,
|
||||||
|
"move_completed_path": "",
|
||||||
|
"prioritize_first_last": false,
|
||||||
|
"remove_at_ratio": false,
|
||||||
|
"stop_at_ratio": false,
|
||||||
|
"stop_ratio": 2.0
|
||||||
|
},
|
||||||
"radarr": {
|
"radarr": {
|
||||||
"apply_max": false,
|
"apply_max": false,
|
||||||
"apply_move_completed": false,
|
"apply_move_completed": false,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<Config>
|
||||||
|
<BindAddress>*</BindAddress>
|
||||||
|
<Port>8686</Port>
|
||||||
|
<SslPort>6868</SslPort>
|
||||||
|
<EnableSsl>False</EnableSsl>
|
||||||
|
<LaunchBrowser>True</LaunchBrowser>
|
||||||
|
<ApiKey>7f677cfdc074414397af53dd633860c5</ApiKey>
|
||||||
|
<AuthenticationMethod>Forms</AuthenticationMethod>
|
||||||
|
<AuthenticationRequired>Enabled</AuthenticationRequired>
|
||||||
|
<Branch>master</Branch>
|
||||||
|
<LogLevel>debug</LogLevel>
|
||||||
|
<SslCertPath></SslCertPath>
|
||||||
|
<SslCertPassword></SslCertPassword>
|
||||||
|
<UrlBase></UrlBase>
|
||||||
|
<InstanceName>Lidarr</InstanceName>
|
||||||
|
<UpdateMechanism>Docker</UpdateMechanism>
|
||||||
|
</Config>
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,38 @@
|
|||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>Test feed</title>
|
||||||
|
<link>http://nginx/custom/sonarr.xml</link>
|
||||||
|
<description>
|
||||||
|
Test
|
||||||
|
</description>
|
||||||
|
<language>en-CA</language>
|
||||||
|
<copyright> Test </copyright>
|
||||||
|
<pubDate>Tue, 5 Nov 2024 22:02:13 -0400</pubDate>
|
||||||
|
<lastBuildDate>Tue, 5 Nov 2024 22:02:13 -0400</lastBuildDate>
|
||||||
|
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
|
||||||
|
<ttl>30</ttl>
|
||||||
|
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>Coldplay-Ghost Stories-(Deluxe Edition)-2014-MTD</title>
|
||||||
|
<description>Test</description>
|
||||||
|
<size>104857600</size>
|
||||||
|
<link>http://nginx/custom/lidarr_bad_single.torrent</link>
|
||||||
|
<guid isPermaLink="false">
|
||||||
|
174674a88c8947f6f9057a23f81efde384ed216cade43564ec450f2cb4677554
|
||||||
|
</guid>
|
||||||
|
<pubDate>Sat, 24 Sep 2022 22:02:13 -0300</pubDate>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>Coldplay-Everyday.Life-2019-C4</title>
|
||||||
|
<description>Test</description>
|
||||||
|
<size>104857600</size>
|
||||||
|
<link>http://nginx/custom/lidarr_bad_pack.torrent</link>
|
||||||
|
<guid isPermaLink="false">
|
||||||
|
174674a88c8947f689057ac3f81efde384ed216cade43564ec450f2cb4677554
|
||||||
|
</guid>
|
||||||
|
<pubDate>Sat, 24 Sep 2022 22:02:13 -0300</pubDate>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1736513625e4:infod5:filesld6:lengthi640e4:pathl27:coldplay-everyday_life.zipxeed6:lengthi640e4:pathl27:coldplay-everyday_life2.mp3eee4:name31:Coldplay-Everyday.Life-2019-C4/12:piece lengthi262144e6:pieces20:#Ú¯§Ñ4OduÎoÎÛ€¹Þ[=~ee
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1736513638e4:infod5:filesld6:lengthi640e4:pathl35:001-coldplay-always_in_my_head.zipxeee4:name49:Coldplay-Ghost Stories-(Deluxe Edition)-2014-MTD/12:piece lengthi262144e6:pieces20:·iV9qæ “Ý)-xÖ©'¦Èò«ee
|
||||||
@@ -1 +1 @@
|
|||||||
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731931728e4:infod5:filesld6:lengthi2604e4:pathl4:Dir15:Dir1111:test11.zipxeed6:lengthi2604e4:pathl4:Dir110:sample.txteed6:lengthi2604e4:pathl4:Dir210:test2.zipxeed6:lengthi2604e4:pathl68:Speak.No.Evil.2024.2160p.MA.WEB-DL.DDP5.1.Atmos.H.265-HHWEB.mkv.zipxeed6:lengthi2604e4:pathl9:test.zipxeee4:name59:Speak.No.Evil.2024.2160p.MA.WEB-DL.DDP5.1.Atmos.H.265-HHWEB12:piece lengthi262144e6:pieces20:w¤ŸÌ³RÇþ'6Fíoð}ä°ee
|
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731931728e4:infod5:filesld6:lengthi2604e4:pathl4:Dir15:Dir1111:test11.zipxeed6:lengthi2604e4:pathl4:Dir110:sample.txteed6:lengthi2604e4:pathl4:Dir210:test2.zipxeed6:lengthi2604e4:pathl68:Speak.No.Evil.2024.2160p.MA.WEB-DL.DDP5.1.Atmos.H.265-HHWEB.mkv.zipxeed6:lengthi2604e4:pathl9:test.zipxeee4:name59:Speak.No.Evil.2024.2160p.MA.WEB-DL.DDP5.1.Atmos.H.265-HHWEB12:piece lengthi262144e6:pieces20:w¤ŸÌ³RÇþ'6Fíoð}ä°7:privatei1eee
|
||||||
@@ -1 +1 @@
|
|||||||
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731931738e4:infod5:filesld6:lengthi2604e4:pathl89:Agatha.All.Along.S01E01.Seekest.Thou.the.Road.2160p.APPS.WEB-DL.DDP5.1.Atmos. - Copy.zipxeed6:lengthi2604e4:pathl4:Dir15:Dir1111:test11.zipxeed6:lengthi2604e4:pathl4:Dir110:sample.txteed6:lengthi2604e4:pathl4:Dir210:test2.zipxeed6:lengthi2604e4:pathl9:test.zipxeee4:name88:Agatha.All.Along.S01E01.Seekest.Thou.the.Road.2160p.APPS.WEB-DL.DDP5.1.Atmos.H.265-VARYG12:piece lengthi262144e6:pieces20:w¤ŸÌ³RÇþ'6Fíoð}ä°ee
|
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731931738e4:infod5:filesld6:lengthi2604e4:pathl89:Agatha.All.Along.S01E01.Seekest.Thou.the.Road.2160p.APPS.WEB-DL.DDP5.1.Atmos. - Copy.zipxeed6:lengthi2604e4:pathl4:Dir15:Dir1111:test11.zipxeed6:lengthi2604e4:pathl4:Dir110:sample.txteed6:lengthi2604e4:pathl4:Dir210:test2.zipxeed6:lengthi2604e4:pathl9:test.zipxeee4:name88:Agatha.All.Along.S01E01.Seekest.Thou.the.Road.2160p.APPS.WEB-DL.DDP5.1.Atmos.H.265-VARYG12:piece lengthi262144e6:pieces20:w¤ŸÌ³RÇþ'6Fíoð}ä°7:privatei1eee
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1736513625e4:infod5:filesld6:lengthi640e4:pathl27:coldplay-everyday_life.zipxeed6:lengthi640e4:pathl27:coldplay-everyday_life2.mp3eee4:name31:Coldplay-Everyday.Life-2019-C4/12:piece lengthi262144e6:pieces20:#Ú¯§Ñ4OduÎoÎÛ€¹Þ[=~ee
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1736513638e4:infod5:filesld6:lengthi640e4:pathl35:001-coldplay-always_in_my_head.zipxeee4:name49:Coldplay-Ghost Stories-(Deluxe Edition)-2014-MTD/12:piece lengthi262144e6:pieces20:·iV9qæ “Ý)-xÖ©'¦Èò«ee
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"lidarr": {
|
||||||
|
"save_path": ""
|
||||||
|
},
|
||||||
"radarr": {
|
"radarr": {
|
||||||
"save_path": ""
|
"save_path": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<Config>
|
||||||
|
<BindAddress>*</BindAddress>
|
||||||
|
<Port>7878</Port>
|
||||||
|
<SslPort>9898</SslPort>
|
||||||
|
<EnableSsl>False</EnableSsl>
|
||||||
|
<LaunchBrowser>True</LaunchBrowser>
|
||||||
|
<ApiKey>8b7454f668e54c5b8f44f56f93969761</ApiKey>
|
||||||
|
<AuthenticationMethod>Forms</AuthenticationMethod>
|
||||||
|
<AuthenticationRequired>Enabled</AuthenticationRequired>
|
||||||
|
<Branch>master</Branch>
|
||||||
|
<LogLevel>debug</LogLevel>
|
||||||
|
<SslCertPath></SslCertPath>
|
||||||
|
<SslCertPassword></SslCertPassword>
|
||||||
|
<UrlBase></UrlBase>
|
||||||
|
<InstanceName>Radarr</InstanceName>
|
||||||
|
<UpdateMechanism>Docker</UpdateMechanism>
|
||||||
|
</Config>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<Config>
|
||||||
|
<BindAddress>*</BindAddress>
|
||||||
|
<Port>8787</Port>
|
||||||
|
<SslPort>6868</SslPort>
|
||||||
|
<EnableSsl>False</EnableSsl>
|
||||||
|
<LaunchBrowser>True</LaunchBrowser>
|
||||||
|
<ApiKey>53388ac405894ef2ac6b82f907f481aa</ApiKey>
|
||||||
|
<AuthenticationMethod>Forms</AuthenticationMethod>
|
||||||
|
<AuthenticationRequired>Enabled</AuthenticationRequired>
|
||||||
|
<Branch>develop</Branch>
|
||||||
|
<LogLevel>debug</LogLevel>
|
||||||
|
<SslCertPath></SslCertPath>
|
||||||
|
<SslCertPassword></SslCertPassword>
|
||||||
|
<UrlBase></UrlBase>
|
||||||
|
<InstanceName>Readarr</InstanceName>
|
||||||
|
<UpdateMechanism>Docker</UpdateMechanism>
|
||||||
|
</Config>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<Config>
|
||||||
|
<BindAddress>*</BindAddress>
|
||||||
|
<Port>8989</Port>
|
||||||
|
<SslPort>9898</SslPort>
|
||||||
|
<EnableSsl>False</EnableSsl>
|
||||||
|
<LaunchBrowser>True</LaunchBrowser>
|
||||||
|
<ApiKey>425d1e713f0c405cbbf359ac0502c1f4</ApiKey>
|
||||||
|
<AuthenticationMethod>Forms</AuthenticationMethod>
|
||||||
|
<AuthenticationRequired>Enabled</AuthenticationRequired>
|
||||||
|
<Branch>main</Branch>
|
||||||
|
<LogLevel>debug</LogLevel>
|
||||||
|
<SslCertPath></SslCertPath>
|
||||||
|
<SslCertPassword></SslCertPassword>
|
||||||
|
<UrlBase></UrlBase>
|
||||||
|
<InstanceName>Sonarr</InstanceName>
|
||||||
|
<UpdateMechanism>Docker</UpdateMechanism>
|
||||||
|
</Config>
|
||||||
@@ -8,10 +8,10 @@
|
|||||||
# ctorrent -t -u "http://tracker:6969/announce" -s example.torrent file_name
|
# ctorrent -t -u "http://tracker:6969/announce" -s example.torrent file_name
|
||||||
|
|
||||||
# api keys
|
# api keys
|
||||||
# sonarr: 96736c3eb3144936b8f1d62d27be8cee
|
# sonarr: 425d1e713f0c405cbbf359ac0502c1f4
|
||||||
# radarr: 705b553732ab4167ab23909305d60600
|
# radarr: 8b7454f668e54c5b8f44f56f93969761
|
||||||
# lidarr: 4bd467b8702a4ecf94f737922dac6481
|
# lidarr: 7f677cfdc074414397af53dd633860c5
|
||||||
# readarr: 51c053efbea34bad90120d5c2237aa85
|
# readarr: 53388ac405894ef2ac6b82f907f481aa
|
||||||
|
|
||||||
services:
|
services:
|
||||||
qbittorrent:
|
qbittorrent:
|
||||||
@@ -186,38 +186,48 @@ services:
|
|||||||
- QUEUECLEANER__RUNSEQUENTIALLY=true
|
- QUEUECLEANER__RUNSEQUENTIALLY=true
|
||||||
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
|
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
|
||||||
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=true
|
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=true
|
||||||
|
- QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE=false
|
||||||
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=file is a sample
|
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=file is a sample
|
||||||
- QUEUECLEANER__STALLED_MAX_STRIKES=5
|
- QUEUECLEANER__STALLED_MAX_STRIKES=5
|
||||||
- QUEUECLEANER__STALLED_IGNORE_PRIVATE=true
|
- QUEUECLEANER__STALLED_IGNORE_PRIVATE=true
|
||||||
|
- QUEUECLEANER__STALLED_DELETE_PRIVATE=false
|
||||||
|
|
||||||
- CONTENTBLOCKER__ENABLED=true
|
- CONTENTBLOCKER__ENABLED=true
|
||||||
- CONTENTBLOCKER__IGNORE_PRIVATE=true
|
- CONTENTBLOCKER__IGNORE_PRIVATE=true
|
||||||
- CONTENTBLOCKER__BLACKLIST__ENABLED=true
|
- CONTENTBLOCKER__DELETE_PRIVATE=false
|
||||||
- CONTENTBLOCKER__BLACKLIST__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist
|
|
||||||
# OR
|
|
||||||
# - CONTENTBLOCKER__WHITELIST__ENABLED=true
|
|
||||||
# - CONTENTBLOCKER__WHITELIST__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/whitelist
|
|
||||||
|
|
||||||
- DOWNLOAD_CLIENT=qbittorrent
|
- DOWNLOAD_CLIENT=qbittorrent
|
||||||
- QBITTORRENT__URL=http://qbittorrent:8080
|
- QBITTORRENT__URL=http://qbittorrent:8080
|
||||||
- QBITTORRENT__USERNAME=test
|
- QBITTORRENT__USERNAME=test
|
||||||
- QBITTORRENT__PASSWORD=testing
|
- QBITTORRENT__PASSWORD=testing
|
||||||
# OR
|
# OR
|
||||||
|
# - DOWNLOAD_CLIENT=deluge
|
||||||
# - DELUGE__URL=http://localhost:8112
|
# - DELUGE__URL=http://localhost:8112
|
||||||
# - DELUGE__PASSWORD=testing
|
# - DELUGE__PASSWORD=testing
|
||||||
# OR
|
# OR
|
||||||
|
# - DOWNLOAD_CLIENT=transmission
|
||||||
# - TRANSMISSION__URL=http://localhost:9091
|
# - TRANSMISSION__URL=http://localhost:9091
|
||||||
# - TRANSMISSION__USERNAME=test
|
# - TRANSMISSION__USERNAME=test
|
||||||
# - TRANSMISSION__PASSWORD=testing
|
# - TRANSMISSION__PASSWORD=testing
|
||||||
|
|
||||||
- SONARR__ENABLED=true
|
- SONARR__ENABLED=true
|
||||||
- SONARR__SEARCHTYPE=Episode
|
- SONARR__SEARCHTYPE=Episode
|
||||||
|
- SONARR__BLOCK__TYPE=blacklist
|
||||||
|
- SONARR__BLOCK__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist
|
||||||
- SONARR__INSTANCES__0__URL=http://sonarr:8989
|
- SONARR__INSTANCES__0__URL=http://sonarr:8989
|
||||||
- SONARR__INSTANCES__0__APIKEY=96736c3eb3144936b8f1d62d27be8cee
|
- SONARR__INSTANCES__0__APIKEY=425d1e713f0c405cbbf359ac0502c1f4
|
||||||
|
|
||||||
- RADARR__ENABLED=true
|
- RADARR__ENABLED=true
|
||||||
|
- RADARR__BLOCK__TYPE=blacklist
|
||||||
|
- RADARR__BLOCK__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist
|
||||||
- RADARR__INSTANCES__0__URL=http://radarr:7878
|
- RADARR__INSTANCES__0__URL=http://radarr:7878
|
||||||
- RADARR__INSTANCES__0__APIKEY=705b553732ab4167ab23909305d60600
|
- RADARR__INSTANCES__0__APIKEY=8b7454f668e54c5b8f44f56f93969761
|
||||||
|
|
||||||
|
- LIDARR__ENABLED=true
|
||||||
|
- LIDARR__BLOCK__TYPE=blacklist
|
||||||
|
- LIDARR__BLOCK__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist # TODO
|
||||||
|
- LIDARR__INSTANCES__0__URL=http://lidarr:8686
|
||||||
|
- LIDARR__INSTANCES__0__APIKEY=7f677cfdc074414397af53dd633860c5
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/cleanuperr/logs:/var/logs
|
- ./data/cleanuperr/logs:/var/logs
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
Reference in New Issue
Block a user