Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a92ebd75c2 | |||
| e6d3929fc9 | |||
| a68e13af35 | |||
| 324c3ace8f | |||
| 3a9d5d9085 | |||
| 89a6eaf0ce | |||
| 027c4a0f4d | |||
| 81990c6768 | |||
| ba02aa0e49 | |||
| 5adbdbd920 | |||
| b3b211d956 | |||
| 279bd6d82d | |||
| 5dced28228 | |||
| 51bdaf64e4 | |||
| 9c8e0ebedc | |||
| e1bea8a8c8 | |||
| a6d3820104 | |||
| 36c793a5fb |
@@ -8,12 +8,49 @@ cleanuperr is a tool for automating the cleanup of unwanted or blocked files in
|
|||||||
|
|
||||||
cleanuperr was created primarily to address malicious files, such as `*.lnk` or `*.zipx`, that were getting stuck in Sonarr/Radarr and required manual intervention. Some of the reddit posts that made cleanuperr come to life can be found [here](https://www.reddit.com/r/sonarr/comments/1gqnx16/psa_sonarr_downloaded_a_virus/), [here](https://www.reddit.com/r/sonarr/comments/1gqwklr/sonar_downloaded_a_mkv_file_which_looked_like_a/), [here](https://www.reddit.com/r/sonarr/comments/1gpw2wa/downloaded_waiting_to_import/) and [here](https://www.reddit.com/r/sonarr/comments/1gpi344/downloads_not_importing_no_files_found/).
|
cleanuperr was created primarily to address malicious files, such as `*.lnk` or `*.zipx`, that were getting stuck in Sonarr/Radarr and required manual intervention. Some of the reddit posts that made cleanuperr come to life can be found [here](https://www.reddit.com/r/sonarr/comments/1gqnx16/psa_sonarr_downloaded_a_virus/), [here](https://www.reddit.com/r/sonarr/comments/1gqwklr/sonar_downloaded_a_mkv_file_which_looked_like_a/), [here](https://www.reddit.com/r/sonarr/comments/1gpw2wa/downloaded_waiting_to_import/) and [here](https://www.reddit.com/r/sonarr/comments/1gpi344/downloads_not_importing_no_files_found/).
|
||||||
|
|
||||||
The tool supports both qBittorrent's built-in exclusion features and its own blocklist-based system. Binaries for all platforms are provided, along with Docker images for easy deployment.
|
> [!IMPORTANT]
|
||||||
|
> **Features:**
|
||||||
|
> - Strike system to mark stalled or downloads stuck in metadata downloading.
|
||||||
|
> - Remove and block downloads that reached a maximum number of strikes.
|
||||||
|
> - Remove downloads blocked by qBittorrent or by cleanuperr's **content blocker**.
|
||||||
|
> - Trigger a search for downloads removed from the *arrs.
|
||||||
|
> - Clean up downloads that have been seeding for a certain amount of time.
|
||||||
|
> - Notify on strike or download removal.
|
||||||
|
> - Ignore certain torrent hashes, categories, tags or trackers from processing.
|
||||||
|
|
||||||
#
|
cleanuperr supports both qBittorrent's built-in exclusion features and its own blocklist-based system. Binaries for all platforms are provided, along with Docker images for easy deployment.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> 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
|
||||||
|
|
||||||
|
## Table of contents:
|
||||||
|
- [Naming choice](#naming-choice)
|
||||||
|
- [Quick Start](#quick-start)
|
||||||
|
- [How it works](#how-it-works)
|
||||||
|
- [Content blocker](#1-content-blocker-will)
|
||||||
|
- [Queue cleaner](#2-queue-cleaner-will)
|
||||||
|
- [Download cleaner](#3-download-cleaner-will)
|
||||||
|
- [Setup](#setup-examples)
|
||||||
|
- [Usage](#usage)
|
||||||
|
- [Docker](#docker)
|
||||||
|
- [Environment Variables](#environment-variables)
|
||||||
|
- [Docker Compose](#docker-compose-example)
|
||||||
|
- [Windows](#windows)
|
||||||
|
- [Linux](#linux)
|
||||||
|
- [MacOS](#macos)
|
||||||
|
- [Credits](#credits)
|
||||||
|
|
||||||
|
## Naming choice
|
||||||
|
|
||||||
|
I've had people asking why it's `cleanuperr` and not `cleanuparr` and that I should change it. This name was intentional.
|
||||||
|
|
||||||
|
I've seen a few discussions on this type of naming and I've decided that I didn't deserve the `arr` moniker since `cleanuperr` is not a fork of `NZB.Drone` and it does not have any affiliation with the arrs. I still wanted to keep the naming style close enough though, to suggest a correlation between them.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> ### Quick Start
|
|
||||||
>
|
>
|
||||||
> 1. **Docker (Recommended)**
|
> 1. **Docker (Recommended)**
|
||||||
> Pull the Docker image from `ghcr.io/flmorg/cleanuperr:latest`.
|
> Pull the Docker image from `ghcr.io/flmorg/cleanuperr:latest`.
|
||||||
@@ -22,15 +59,11 @@ The tool supports both qBittorrent's built-in exclusion features and its own blo
|
|||||||
> Use the Unraid Community App.
|
> Use the Unraid Community App.
|
||||||
>
|
>
|
||||||
> 3. **Manual Installation (if you're not using Docker)**
|
> 3. **Manual Installation (if you're not using Docker)**
|
||||||
> More details [here](#binaries-if-youre-not-using-docker).
|
> Go to [Windows](#windows), [Linux](#linux) or [MacOS](#macos).
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> Refer to the [Environment variables](#Environment-variables) section for detailed configuration instructions and the [Setup](#Setup) section for an in-depth explanation of the cleanup process.
|
> Refer to the [Environment variables](#environment-variables) section for detailed configuration instructions and the [Setup examples](#setup-examples) section for an in-depth explanation of the cleanup process.
|
||||||
|
|
||||||
## Key features
|
|
||||||
- Marks unwanted files as skip/unwanted in the download client.
|
|
||||||
- Automatically strikes stalled or stuck downloads.
|
|
||||||
- Removes and blocks downloads that reached the maximum number of strikes or are marked as unwanted by the download client or by cleanuperr and triggers a search for removed downloads.
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Only the **latest versions** of the following apps are supported, or earlier versions that have the same API as the latest version:
|
> Only the **latest versions** of the following apps are supported, or earlier versions that have the same API as the latest version:
|
||||||
@@ -41,13 +74,9 @@ The tool supports both qBittorrent's built-in exclusion features and its own blo
|
|||||||
> - Radarr
|
> - Radarr
|
||||||
> - Lidarr
|
> - Lidarr
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# How it works
|
# How it works
|
||||||
|
|
||||||
1. **Content blocker** will:
|
#### 1. **Content blocker** will:
|
||||||
- Run every 5 minutes (or configured cron).
|
- Run every 5 minutes (or configured cron).
|
||||||
- Process all items in the *arr queue.
|
- Process all items in the *arr queue.
|
||||||
- Find the corresponding item from the download client for each queue item.
|
- Find the corresponding item from the download client for each queue item.
|
||||||
@@ -58,7 +87,7 @@ This tool is actively developed and still a work in progress, so using the `late
|
|||||||
- It will be removed from the *arr's queue and blocked.
|
- It will be removed from the *arr's queue and blocked.
|
||||||
- It will be deleted from the download client.
|
- It will be deleted from the download client.
|
||||||
- A new search will be triggered for the *arr item.
|
- 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, or right after `content blocker`).
|
- 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 metadata downloading** or **failed to be imported**.
|
- Check each queue item if it is **stalled (download speed is 0)**, **stuck in metadata downloading** or **failed to be imported**.
|
||||||
@@ -71,11 +100,11 @@ This tool is actively developed and still a work in progress, so using the `late
|
|||||||
- It will be removed from the *arr's queue and blocked.
|
- It will be removed from the *arr's queue and blocked.
|
||||||
- It will be deleted from the download client.
|
- It will be deleted from the download client.
|
||||||
- A new search will be triggered for the *arr item.
|
- A new search will be triggered for the *arr item.
|
||||||
3. **Download cleaner** will:
|
#### 3. **Download cleaner** will:
|
||||||
- Run every hour (or configured cron).
|
- Run every hour (or configured cron).
|
||||||
- Automatically clean up downloads that have been seeding for a certain amount of time.
|
- Automatically clean up downloads that have been seeding for a certain amount of time.
|
||||||
|
|
||||||
# Setup
|
# Setup examples
|
||||||
|
|
||||||
## Using qBittorrent's built-in feature (works only with qBittorrent)
|
## Using qBittorrent's built-in feature (works only with qBittorrent)
|
||||||
|
|
||||||
@@ -90,7 +119,7 @@ This tool is actively developed and still a work in progress, so using the `late
|
|||||||
## 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 [Arr variables](#Arr-variables) section.
|
2. Configure and enable either a **blacklist** or a **whitelist** as described in the [Arr variables](variables.md#Arr-settings) 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.
|
||||||
@@ -102,11 +131,31 @@ This tool is actively developed and still a work in progress, so using the `late
|
|||||||
3. Optionally set failed import message patterns to ignore using `QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__<NUMBER>`.
|
3. Optionally set failed import message patterns to ignore using `QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__<NUMBER>`.
|
||||||
4. Set `DOWNLOAD_CLIENT` to `none`.
|
4. Set `DOWNLOAD_CLIENT` to `none`.
|
||||||
|
|
||||||
**No other action involving a download client would work (e.g. content blocking, removing stalled downloads, excluding private trackers).**
|
> [!WARNING]
|
||||||
|
> When `DOWNLOAD_CLIENT=none`, no other action involving a download client would work (e.g. content blocking, removing stalled downloads, excluding private trackers).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Docker compose yaml
|
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/docker.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">Docker</span>
|
||||||
|
|
||||||
|
|
||||||
|
### **Environment variables**
|
||||||
|
|
||||||
|
**Jump to:**
|
||||||
|
- [General settings](variables.md#general-settings)
|
||||||
|
- [Queue Cleaner settings](variables.md#queue-cleaner-settings)
|
||||||
|
- [Content Blocker settings](variables.md#content-blocker-settings)
|
||||||
|
- [Download Cleaner settings](variables.md#download-cleaner-settings)
|
||||||
|
- [Download Client settings](variables.md#download-client-settings)
|
||||||
|
- [Arr settings](variables.md#arr-settings)
|
||||||
|
- [Notification settings](variables.md#notification-settings)
|
||||||
|
- [Advanced settings](variables.md#advanced-settings)
|
||||||
|
|
||||||
|
### Docker compose example
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
>
|
||||||
|
> This example contains all settings and should be modified to fit your needs.
|
||||||
|
|
||||||
```
|
```
|
||||||
version: "3.3"
|
version: "3.3"
|
||||||
@@ -116,7 +165,9 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./cleanuperr/logs:/var/logs
|
- ./cleanuperr/logs:/var/logs
|
||||||
|
- ./cleanuperr/ignored.txt:/ignored.txt
|
||||||
environment:
|
environment:
|
||||||
|
- TZ=America/New_York
|
||||||
- DRY_RUN=false
|
- DRY_RUN=false
|
||||||
|
|
||||||
- LOGGING__LOGLEVEL=Information
|
- LOGGING__LOGLEVEL=Information
|
||||||
@@ -129,6 +180,7 @@ services:
|
|||||||
- TRIGGERS__DOWNLOADCLEANER=0 0 * * * ?
|
- TRIGGERS__DOWNLOADCLEANER=0 0 * * * ?
|
||||||
|
|
||||||
- QUEUECLEANER__ENABLED=true
|
- QUEUECLEANER__ENABLED=true
|
||||||
|
- QUEUECLEANER__IGNORED_DOWNLOADS_PATH=/ignored.txt
|
||||||
- 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
|
||||||
@@ -141,10 +193,12 @@ services:
|
|||||||
- QUEUECLEANER__STALLED_DELETE_PRIVATE=false
|
- QUEUECLEANER__STALLED_DELETE_PRIVATE=false
|
||||||
|
|
||||||
- CONTENTBLOCKER__ENABLED=true
|
- CONTENTBLOCKER__ENABLED=true
|
||||||
|
- CONTENTBLOCKER__IGNORED_DOWNLOADS_PATH=/ignored.txt
|
||||||
- CONTENTBLOCKER__IGNORE_PRIVATE=false
|
- CONTENTBLOCKER__IGNORE_PRIVATE=false
|
||||||
- CONTENTBLOCKER__DELETE_PRIVATE=false
|
- CONTENTBLOCKER__DELETE_PRIVATE=false
|
||||||
|
|
||||||
- DOWNLOADCLEANER__ENABLED=true
|
- DOWNLOADCLEANER__ENABLED=true
|
||||||
|
- DOWNLOADCLEANER__IGNORED_DOWNLOADS_PATH=/ignored.txt
|
||||||
- DOWNLOADCLEANER__DELETE_PRIVATE=false
|
- DOWNLOADCLEANER__DELETE_PRIVATE=false
|
||||||
- DOWNLOADCLEANER__CATEGORIES__0__NAME=tv-sonarr
|
- DOWNLOADCLEANER__CATEGORIES__0__NAME=tv-sonarr
|
||||||
- DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO=-1
|
- DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO=-1
|
||||||
@@ -204,28 +258,48 @@ services:
|
|||||||
- NOTIFIARR__CHANNEL_ID=discord_channel_id
|
- NOTIFIARR__CHANNEL_ID=discord_channel_id
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment variables
|
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/windows.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">Windows</span>
|
||||||
|
|
||||||
Jump to:
|
1. Download the zip file from [releases](https://github.com/flmorg/cleanuperr/releases).
|
||||||
- [General settings](variables.md#general-settings)
|
2. Extract the zip file into `C:\example\directory`.
|
||||||
- [Queue Cleaner settings](variables.md#queue-cleaner-settings)
|
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [here](#environment-variables).
|
||||||
- [Content Blocker settings](variables.md#content-blocker-settings)
|
4. Execute `cleanuperr.exe`.
|
||||||
- [Download Cleaner settings](variables.md#download-cleaner-settings)
|
|
||||||
- [Download Client settings](variables.md#download-client-settings)
|
|
||||||
- [Arr settings](variables.md#arr-settings)
|
|
||||||
- [Notification settings](variables.md#notification-settings)
|
|
||||||
- [Advanced settings](variables.md#advanced-settings)
|
|
||||||
|
|
||||||
### Binaries (if you're not using Docker)
|
|
||||||
|
|
||||||
1. Download the binaries from [releases](https://github.com/flmorg/cleanuperr/releases).
|
|
||||||
2. Extract them from the zip file.
|
|
||||||
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [above](#environment-variables).
|
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> ### Run as a Windows Service
|
> ### Run as a Windows Service
|
||||||
> Check out this stackoverflow answer on how to do it: https://stackoverflow.com/a/15719678
|
> Check out this stackoverflow answer on how to do it: https://stackoverflow.com/a/15719678
|
||||||
|
|
||||||
|
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/linux.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">Linux</span>
|
||||||
|
|
||||||
|
1. Download the zip file from [releases](https://github.com/flmorg/cleanuperr/releases).
|
||||||
|
2. Extract the zip file into `/example/directory`.
|
||||||
|
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [here](#environment-variables).
|
||||||
|
4. Open a terminal and execute these commands:
|
||||||
|
```
|
||||||
|
cd /example/directory
|
||||||
|
chmod +x cleanuperr
|
||||||
|
./cleanuperr
|
||||||
|
```
|
||||||
|
|
||||||
|
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/apple.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">MacOS</span>
|
||||||
|
|
||||||
|
1. Download the zip file from [releases](https://github.com/flmorg/cleanuperr/releases).
|
||||||
|
2. Extract the zip file into `/example/directory`.
|
||||||
|
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [here](#environment-variables).
|
||||||
|
4. Open a terminal and execute these commands:
|
||||||
|
```
|
||||||
|
cd /example/directory
|
||||||
|
chmod +x cleanuperr
|
||||||
|
./cleanuperr
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Some people have experienced problems when trying to execute cleanuperr on MacOS because the system actively blocked the file for not being signed.
|
||||||
|
> As per [this](), you may need to also execute this command:
|
||||||
|
> ```
|
||||||
|
> codesign --sign - --force --preserve-metadata=entitlements,requirements,flags,runtime /example/directory/cleanuperr
|
||||||
|
> ```
|
||||||
|
|
||||||
# Credits
|
# Credits
|
||||||
Special thanks for inspiration go to:
|
Special thanks for inspiration go to:
|
||||||
- [ThijmenGThN/swaparr](https://github.com/ThijmenGThN/swaparr)
|
- [ThijmenGThN/swaparr](https://github.com/ThijmenGThN/swaparr)
|
||||||
@@ -237,4 +311,3 @@ Special thanks for inspiration go to:
|
|||||||
If I made your life just a tiny bit easier, consider buying me a coffee!
|
If I made your life just a tiny bit easier, consider buying me a coffee!
|
||||||
|
|
||||||
<a href="https://buymeacoffee.com/flaminel" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
<a href="https://buymeacoffee.com/flaminel" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
||||||
|
|
||||||
|
|||||||
+27
-1
@@ -10,6 +10,9 @@ deployment:
|
|||||||
repository: ghcr.io/flmorg/cleanuperr
|
repository: ghcr.io/flmorg/cleanuperr
|
||||||
tag: latest
|
tag: latest
|
||||||
env:
|
env:
|
||||||
|
- name: DRY_RUN
|
||||||
|
value: "false"
|
||||||
|
|
||||||
- name: LOGGING__LOGLEVEL
|
- name: LOGGING__LOGLEVEL
|
||||||
value: Debug
|
value: Debug
|
||||||
- name: LOGGING__FILE__ENABLED
|
- name: LOGGING__FILE__ENABLED
|
||||||
@@ -18,6 +21,7 @@ deployment:
|
|||||||
value: /var/logs
|
value: /var/logs
|
||||||
- name: LOGGING__ENHANCED
|
- name: LOGGING__ENHANCED
|
||||||
value: "true"
|
value: "true"
|
||||||
|
|
||||||
- name: TRIGGERS__QUEUECLEANER
|
- name: TRIGGERS__QUEUECLEANER
|
||||||
value: 0 0/5 * * * ?
|
value: 0 0/5 * * * ?
|
||||||
- name: TRIGGERS__CONTENTBLOCKER
|
- name: TRIGGERS__CONTENTBLOCKER
|
||||||
@@ -47,6 +51,9 @@ deployment:
|
|||||||
- name: CONTENTBLOCKER__DELETE_PRIVATE
|
- name: CONTENTBLOCKER__DELETE_PRIVATE
|
||||||
value: "false"
|
value: "false"
|
||||||
|
|
||||||
|
- name: DOWNLOADCLEANER__ENABLED
|
||||||
|
value: "false"
|
||||||
|
|
||||||
- name: DOWNLOAD_CLIENT
|
- name: DOWNLOAD_CLIENT
|
||||||
value: qbittorrent
|
value: qbittorrent
|
||||||
- name: QBITTORRENT__URL
|
- name: QBITTORRENT__URL
|
||||||
@@ -75,6 +82,17 @@ deployment:
|
|||||||
value: http://service.radarr-low-res.svc.cluster.local
|
value: http://service.radarr-low-res.svc.cluster.local
|
||||||
- name: RADARR__INSTANCES__1__URL
|
- name: RADARR__INSTANCES__1__URL
|
||||||
value: http://service.radarr-high-res.svc.cluster.local
|
value: http://service.radarr-high-res.svc.cluster.local
|
||||||
|
|
||||||
|
- name: NOTIFIARR__ON_IMPORT_FAILED_STRIKE
|
||||||
|
value: "true"
|
||||||
|
- name: NOTIFIARR__ON_STALLED_STRIKE
|
||||||
|
value: "true"
|
||||||
|
- name: NOTIFIARR__ON_QUEUE_ITEM_DELETED
|
||||||
|
value: "true"
|
||||||
|
- name: NOTIFIARR__ON_DOWNLOAD_CLEANED
|
||||||
|
value: "true"
|
||||||
|
- name: NOTIFIARR__CHANNEL_ID
|
||||||
|
value: "1340708411259748413"
|
||||||
envFromSecret:
|
envFromSecret:
|
||||||
- secretName: qbit-auth
|
- secretName: qbit-auth
|
||||||
envs:
|
envs:
|
||||||
@@ -94,6 +112,10 @@ deployment:
|
|||||||
key: RDRL_API_KEY
|
key: RDRL_API_KEY
|
||||||
- name: RADARR__INSTANCES__1__APIKEY
|
- name: RADARR__INSTANCES__1__APIKEY
|
||||||
key: RDRH_API_KEY
|
key: RDRH_API_KEY
|
||||||
|
- secretName: notifiarr-auth
|
||||||
|
envs:
|
||||||
|
- name: NOTIFIARR__API_KEY
|
||||||
|
key: API_KEY
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 0m
|
cpu: 0m
|
||||||
@@ -133,4 +155,8 @@ vaultSecrets:
|
|||||||
path: secrets/sonarr
|
path: secrets/sonarr
|
||||||
templates:
|
templates:
|
||||||
SNRL_API_KEY: "{% .Secrets.low_api_key %}"
|
SNRL_API_KEY: "{% .Secrets.low_api_key %}"
|
||||||
SNRH_API_KEY: "{% .Secrets.high_api_key %}"
|
SNRH_API_KEY: "{% .Secrets.high_api_key %}"
|
||||||
|
- name: notifiarr-auth
|
||||||
|
path: secrets/notifiarr
|
||||||
|
templates:
|
||||||
|
API_KEY: "{% .Secrets.passthrough_api_key %}"
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Common.Configuration.ContentBlocker;
|
namespace Common.Configuration.ContentBlocker;
|
||||||
|
|
||||||
public sealed record ContentBlockerConfig : IJobConfig
|
public sealed record ContentBlockerConfig : IJobConfig, IIgnoredDownloadsConfig
|
||||||
{
|
{
|
||||||
public const string SectionName = "ContentBlocker";
|
public const string SectionName = "ContentBlocker";
|
||||||
|
|
||||||
@@ -13,6 +13,9 @@ public sealed record ContentBlockerConfig : IJobConfig
|
|||||||
|
|
||||||
[ConfigurationKeyName("DELETE_PRIVATE")]
|
[ConfigurationKeyName("DELETE_PRIVATE")]
|
||||||
public bool DeletePrivate { get; init; }
|
public bool DeletePrivate { get; init; }
|
||||||
|
|
||||||
|
[ConfigurationKeyName("IGNORED_DOWNLOADS_PATH")]
|
||||||
|
public string? IgnoredDownloadsPath { get; init; }
|
||||||
|
|
||||||
public void Validate()
|
public void Validate()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using Microsoft.Extensions.Configuration;
|
|||||||
|
|
||||||
namespace Common.Configuration.DownloadCleaner;
|
namespace Common.Configuration.DownloadCleaner;
|
||||||
|
|
||||||
public sealed record DownloadCleanerConfig : IJobConfig
|
public sealed record DownloadCleanerConfig : IJobConfig, IIgnoredDownloadsConfig
|
||||||
{
|
{
|
||||||
public const string SectionName = "DownloadCleaner";
|
public const string SectionName = "DownloadCleaner";
|
||||||
|
|
||||||
@@ -12,7 +12,10 @@ public sealed record DownloadCleanerConfig : IJobConfig
|
|||||||
public List<Category>? Categories { get; init; }
|
public List<Category>? Categories { get; init; }
|
||||||
|
|
||||||
[ConfigurationKeyName("DELETE_PRIVATE")]
|
[ConfigurationKeyName("DELETE_PRIVATE")]
|
||||||
public bool DeletePrivate { get; set; }
|
public bool DeletePrivate { get; init; }
|
||||||
|
|
||||||
|
[ConfigurationKeyName("IGNORED_DOWNLOADS_PATH")]
|
||||||
|
public string? IgnoredDownloadsPath { get; init; }
|
||||||
|
|
||||||
public void Validate()
|
public void Validate()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Common.Configuration;
|
||||||
|
|
||||||
|
public interface IIgnoredDownloadsConfig
|
||||||
|
{
|
||||||
|
string? IgnoredDownloadsPath { get; }
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Common.Exceptions;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Common.Configuration.QueueCleaner;
|
namespace Common.Configuration.QueueCleaner;
|
||||||
|
|
||||||
public sealed record QueueCleanerConfig : IJobConfig
|
public sealed record QueueCleanerConfig : IJobConfig, IIgnoredDownloadsConfig
|
||||||
{
|
{
|
||||||
public const string SectionName = "QueueCleaner";
|
public const string SectionName = "QueueCleaner";
|
||||||
|
|
||||||
@@ -10,6 +11,9 @@ public sealed record QueueCleanerConfig : IJobConfig
|
|||||||
|
|
||||||
public required bool RunSequentially { get; init; }
|
public required bool RunSequentially { get; init; }
|
||||||
|
|
||||||
|
[ConfigurationKeyName("IGNORED_DOWNLOADS_PATH")]
|
||||||
|
public string? IgnoredDownloadsPath { get; init; }
|
||||||
|
|
||||||
[ConfigurationKeyName("IMPORT_FAILED_MAX_STRIKES")]
|
[ConfigurationKeyName("IMPORT_FAILED_MAX_STRIKES")]
|
||||||
public ushort ImportFailedMaxStrikes { get; init; }
|
public ushort ImportFailedMaxStrikes { get; init; }
|
||||||
|
|
||||||
@@ -36,5 +40,14 @@ public sealed record QueueCleanerConfig : IJobConfig
|
|||||||
|
|
||||||
public void Validate()
|
public void Validate()
|
||||||
{
|
{
|
||||||
|
if (ImportFailedMaxStrikes is > 0 and < 3)
|
||||||
|
{
|
||||||
|
throw new ValidationException("the minimum value for IMPORT_FAILED_MAX_STRIKES must be 3");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StalledMaxStrikes is > 0 and < 3)
|
||||||
|
{
|
||||||
|
throw new ValidationException("the minimum value for STALLED_MAX_STRIKES must be 3");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,4 +23,11 @@ public sealed record TorrentStatus
|
|||||||
public long SeedingTime { get; init; }
|
public long SeedingTime { get; init; }
|
||||||
|
|
||||||
public float Ratio { get; init; }
|
public float Ratio { get; init; }
|
||||||
|
|
||||||
|
public required IReadOnlyList<Tracker> Trackers { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record Tracker
|
||||||
|
{
|
||||||
|
public required Uri Url { get; init; }
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using Castle.DynamicProxy;
|
|
||||||
using Common.Configuration.General;
|
using Common.Configuration.General;
|
||||||
using Common.Helpers;
|
using Common.Helpers;
|
||||||
using Infrastructure.Interceptors;
|
|
||||||
using Infrastructure.Verticals.DownloadClient.Deluge;
|
using Infrastructure.Verticals.DownloadClient.Deluge;
|
||||||
using Infrastructure.Verticals.Notifications.Consumers;
|
using Infrastructure.Verticals.Notifications.Consumers;
|
||||||
using Infrastructure.Verticals.Notifications.Models;
|
using Infrastructure.Verticals.Notifications.Models;
|
||||||
@@ -42,8 +40,7 @@ public static class MainDI
|
|||||||
e.PrefetchCount = 1;
|
e.PrefetchCount = 1;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
.AddDryRunInterceptor();
|
|
||||||
|
|
||||||
private static IServiceCollection AddHttpClients(this IServiceCollection services, IConfiguration configuration)
|
private static IServiceCollection AddHttpClients(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
@@ -65,7 +62,7 @@ public static class MainDI
|
|||||||
services
|
services
|
||||||
.AddHttpClient(nameof(DelugeService), x =>
|
.AddHttpClient(nameof(DelugeService), x =>
|
||||||
{
|
{
|
||||||
x.Timeout = TimeSpan.FromSeconds(5);
|
x.Timeout = TimeSpan.FromSeconds(config.Timeout);
|
||||||
})
|
})
|
||||||
.ConfigurePrimaryHttpMessageHandler(_ =>
|
.ConfigurePrimaryHttpMessageHandler(_ =>
|
||||||
{
|
{
|
||||||
@@ -91,31 +88,4 @@ public static class MainDI
|
|||||||
.OrResult(response => !response.IsSuccessStatusCode && response.StatusCode != HttpStatusCode.Unauthorized)
|
.OrResult(response => !response.IsSuccessStatusCode && response.StatusCode != HttpStatusCode.Unauthorized)
|
||||||
.WaitAndRetryAsync(config.MaxRetries, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)))
|
.WaitAndRetryAsync(config.MaxRetries, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)))
|
||||||
);
|
);
|
||||||
|
|
||||||
private static IServiceCollection AddDryRunInterceptor(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services
|
|
||||||
.Where(s => s.ServiceType != typeof(IDryRunService) && typeof(IDryRunService).IsAssignableFrom(s.ServiceType))
|
|
||||||
.ToList()
|
|
||||||
.ForEach(service =>
|
|
||||||
{
|
|
||||||
services.Decorate(service.ServiceType, (target, svc) =>
|
|
||||||
{
|
|
||||||
ProxyGenerator proxyGenerator = new();
|
|
||||||
DryRunAsyncInterceptor interceptor = svc.GetRequiredService<DryRunAsyncInterceptor>();
|
|
||||||
|
|
||||||
object implementation = proxyGenerator.CreateClassProxyWithTarget(
|
|
||||||
service.ServiceType,
|
|
||||||
target,
|
|
||||||
interceptor
|
|
||||||
);
|
|
||||||
|
|
||||||
((IInterceptedService)target).Proxy = implementation;
|
|
||||||
|
|
||||||
return implementation;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ public static class NotificationsDI
|
|||||||
.Configure<NotifiarrConfig>(configuration.GetSection(NotifiarrConfig.SectionName))
|
.Configure<NotifiarrConfig>(configuration.GetSection(NotifiarrConfig.SectionName))
|
||||||
.AddTransient<INotifiarrProxy, NotifiarrProxy>()
|
.AddTransient<INotifiarrProxy, NotifiarrProxy>()
|
||||||
.AddTransient<INotificationProvider, NotifiarrProvider>()
|
.AddTransient<INotificationProvider, NotifiarrProvider>()
|
||||||
.AddTransient<NotificationPublisher>()
|
.AddTransient<INotificationPublisher, NotificationPublisher>()
|
||||||
.AddTransient<INotificationFactory, NotificationFactory>()
|
.AddTransient<INotificationFactory, NotificationFactory>()
|
||||||
.AddTransient<NotificationService>();
|
.AddTransient<NotificationService>();
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ public static class QuartzDI
|
|||||||
if (contentBlockerConfig?.Enabled is true && queueCleanerConfig is { Enabled: true, RunSequentially: true })
|
if (contentBlockerConfig?.Enabled is true && queueCleanerConfig is { Enabled: true, RunSequentially: true })
|
||||||
{
|
{
|
||||||
q.AddJob<QueueCleaner>(queueCleanerConfig, string.Empty);
|
q.AddJob<QueueCleaner>(queueCleanerConfig, string.Empty);
|
||||||
q.AddJobListener(new JobChainingListener(nameof(QueueCleaner)));
|
q.AddJobListener(new JobChainingListener(nameof(ContentBlocker), nameof(QueueCleaner)));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
using Infrastructure.Interceptors;
|
using Common.Configuration.ContentBlocker;
|
||||||
|
using Common.Configuration.DownloadCleaner;
|
||||||
|
using Common.Configuration.QueueCleaner;
|
||||||
|
using Infrastructure.Interceptors;
|
||||||
|
using Infrastructure.Providers;
|
||||||
using Infrastructure.Verticals.Arr;
|
using Infrastructure.Verticals.Arr;
|
||||||
using Infrastructure.Verticals.ContentBlocker;
|
using Infrastructure.Verticals.ContentBlocker;
|
||||||
using Infrastructure.Verticals.DownloadCleaner;
|
using Infrastructure.Verticals.DownloadCleaner;
|
||||||
@@ -15,7 +19,7 @@ public static class ServicesDI
|
|||||||
{
|
{
|
||||||
public static IServiceCollection AddServices(this IServiceCollection services) =>
|
public static IServiceCollection AddServices(this IServiceCollection services) =>
|
||||||
services
|
services
|
||||||
.AddTransient<DryRunAsyncInterceptor>()
|
.AddTransient<IDryRunInterceptor, DryRunInterceptor>()
|
||||||
.AddTransient<SonarrClient>()
|
.AddTransient<SonarrClient>()
|
||||||
.AddTransient<RadarrClient>()
|
.AddTransient<RadarrClient>()
|
||||||
.AddTransient<LidarrClient>()
|
.AddTransient<LidarrClient>()
|
||||||
@@ -30,5 +34,8 @@ public static class ServicesDI
|
|||||||
.AddTransient<ArrQueueIterator>()
|
.AddTransient<ArrQueueIterator>()
|
||||||
.AddTransient<DownloadServiceFactory>()
|
.AddTransient<DownloadServiceFactory>()
|
||||||
.AddSingleton<BlocklistProvider>()
|
.AddSingleton<BlocklistProvider>()
|
||||||
.AddSingleton<IStriker, Striker>();
|
.AddSingleton<IStriker, Striker>()
|
||||||
|
.AddSingleton<IgnoredDownloadsProvider<QueueCleanerConfig>>()
|
||||||
|
.AddSingleton<IgnoredDownloadsProvider<ContentBlockerConfig>>()
|
||||||
|
.AddSingleton<IgnoredDownloadsProvider<DownloadCleanerConfig>>();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Executable;
|
||||||
|
|
||||||
|
public static class HostExtensions
|
||||||
|
{
|
||||||
|
public static IHost Init(this IHost host)
|
||||||
|
{
|
||||||
|
ILogger<Program> logger = host.Services.GetRequiredService<ILogger<Program>>();
|
||||||
|
|
||||||
|
Version? version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
version is null
|
||||||
|
? "cleanuperr version not detected"
|
||||||
|
: $"cleanuperr v{version.Major}.{version.Minor}.{version.Build}"
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.LogInformation("timezone: {tz}", TimeZoneInfo.Local.DisplayName);
|
||||||
|
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Reflection;
|
using Executable;
|
||||||
using Executable.DependencyInjection;
|
using Executable.DependencyInjection;
|
||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
@@ -7,15 +7,6 @@ builder.Services.AddInfrastructure(builder.Configuration);
|
|||||||
builder.Logging.AddLogging(builder.Configuration);
|
builder.Logging.AddLogging(builder.Configuration);
|
||||||
|
|
||||||
var host = builder.Build();
|
var host = builder.Build();
|
||||||
|
host.Init();
|
||||||
var logger = host.Services.GetRequiredService<ILogger<Program>>();
|
|
||||||
|
|
||||||
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
|
||||||
|
|
||||||
logger.LogInformation(
|
|
||||||
version is null
|
|
||||||
? "cleanuperr version not detected"
|
|
||||||
: $"cleanuperr v{version.Major}.{version.Minor}.{version.Build}"
|
|
||||||
);
|
|
||||||
|
|
||||||
host.Run();
|
host.Run();
|
||||||
@@ -18,11 +18,13 @@
|
|||||||
"ContentBlocker": {
|
"ContentBlocker": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"IGNORE_PRIVATE": true,
|
"IGNORE_PRIVATE": true,
|
||||||
"DELETE_PRIVATE": false
|
"DELETE_PRIVATE": false,
|
||||||
|
"IGNORED_DOWNLOADS_PATH": "../test/data/cleanuperr/ignored_downloads"
|
||||||
},
|
},
|
||||||
"QueueCleaner": {
|
"QueueCleaner": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"RunSequentially": true,
|
"RunSequentially": true,
|
||||||
|
"IGNORED_DOWNLOADS_PATH": "../test/data/cleanuperr/ignored_downloads",
|
||||||
"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_DELETE_PRIVATE": false,
|
||||||
@@ -44,7 +46,8 @@
|
|||||||
"MIN_SEED_TIME": 0,
|
"MIN_SEED_TIME": 0,
|
||||||
"MAX_SEED_TIME": -1
|
"MAX_SEED_TIME": -1
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"IGNORED_DOWNLOADS_PATH": "../test/data/cleanuperr/ignored_downloads"
|
||||||
},
|
},
|
||||||
"DOWNLOAD_CLIENT": "qbittorrent",
|
"DOWNLOAD_CLIENT": "qbittorrent",
|
||||||
"qBittorrent": {
|
"qBittorrent": {
|
||||||
|
|||||||
@@ -17,11 +17,13 @@
|
|||||||
},
|
},
|
||||||
"ContentBlocker": {
|
"ContentBlocker": {
|
||||||
"Enabled": false,
|
"Enabled": false,
|
||||||
"IGNORE_PRIVATE": false
|
"IGNORE_PRIVATE": false,
|
||||||
|
"IGNORED_DOWNLOADS_PATH": ""
|
||||||
},
|
},
|
||||||
"QueueCleaner": {
|
"QueueCleaner": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"RunSequentially": true,
|
"RunSequentially": true,
|
||||||
|
"IGNORED_DOWNLOADS_PATH": "",
|
||||||
"IMPORT_FAILED_MAX_STRIKES": 0,
|
"IMPORT_FAILED_MAX_STRIKES": 0,
|
||||||
"IMPORT_FAILED_IGNORE_PRIVATE": false,
|
"IMPORT_FAILED_IGNORE_PRIVATE": false,
|
||||||
"IMPORT_FAILED_DELETE_PRIVATE": false,
|
"IMPORT_FAILED_DELETE_PRIVATE": false,
|
||||||
@@ -34,7 +36,8 @@
|
|||||||
"DownloadCleaner": {
|
"DownloadCleaner": {
|
||||||
"Enabled": false,
|
"Enabled": false,
|
||||||
"DELETE_PRIVATE": false,
|
"DELETE_PRIVATE": false,
|
||||||
"CATEGORIES": []
|
"CATEGORIES": [],
|
||||||
|
"IGNORED_DOWNLOADS_PATH": ""
|
||||||
},
|
},
|
||||||
"DOWNLOAD_CLIENT": "none",
|
"DOWNLOAD_CLIENT": "none",
|
||||||
"qBittorrent": {
|
"qBittorrent": {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Common.Configuration.ContentBlocker;
|
using Common.Configuration.ContentBlocker;
|
||||||
using Common.Configuration.DownloadCleaner;
|
using Common.Configuration.DownloadCleaner;
|
||||||
using Common.Configuration.QueueCleaner;
|
using Common.Configuration.QueueCleaner;
|
||||||
|
using Infrastructure.Interceptors;
|
||||||
using Infrastructure.Verticals.ContentBlocker;
|
using Infrastructure.Verticals.ContentBlocker;
|
||||||
using Infrastructure.Verticals.DownloadClient;
|
using Infrastructure.Verticals.DownloadClient;
|
||||||
using Infrastructure.Verticals.ItemStriker;
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
@@ -53,7 +54,8 @@ public class DownloadServiceFixture : IDisposable
|
|||||||
downloadCleanerOptions.Value.Returns(new DownloadCleanerConfig());
|
downloadCleanerOptions.Value.Returns(new DownloadCleanerConfig());
|
||||||
|
|
||||||
var filenameEvaluator = Substitute.For<IFilenameEvaluator>();
|
var filenameEvaluator = Substitute.For<IFilenameEvaluator>();
|
||||||
var notifier = Substitute.For<NotificationPublisher>();
|
var notifier = Substitute.For<INotificationPublisher>();
|
||||||
|
var dryRunInterceptor = Substitute.For<IDryRunInterceptor>();
|
||||||
|
|
||||||
return new TestDownloadService(
|
return new TestDownloadService(
|
||||||
Logger,
|
Logger,
|
||||||
@@ -63,7 +65,8 @@ public class DownloadServiceFixture : IDisposable
|
|||||||
Cache,
|
Cache,
|
||||||
filenameEvaluator,
|
filenameEvaluator,
|
||||||
Striker,
|
Striker,
|
||||||
notifier
|
notifier,
|
||||||
|
dryRunInterceptor
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Text.RegularExpressions;
|
|||||||
using Common.Configuration.ContentBlocker;
|
using Common.Configuration.ContentBlocker;
|
||||||
using Common.Configuration.DownloadCleaner;
|
using Common.Configuration.DownloadCleaner;
|
||||||
using Common.Configuration.QueueCleaner;
|
using Common.Configuration.QueueCleaner;
|
||||||
|
using Infrastructure.Interceptors;
|
||||||
using Infrastructure.Verticals.ContentBlocker;
|
using Infrastructure.Verticals.ContentBlocker;
|
||||||
using Infrastructure.Verticals.DownloadClient;
|
using Infrastructure.Verticals.DownloadClient;
|
||||||
using Infrastructure.Verticals.ItemStriker;
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
@@ -23,20 +24,24 @@ public class TestDownloadService : DownloadService
|
|||||||
IMemoryCache cache,
|
IMemoryCache cache,
|
||||||
IFilenameEvaluator filenameEvaluator,
|
IFilenameEvaluator filenameEvaluator,
|
||||||
IStriker striker,
|
IStriker striker,
|
||||||
NotificationPublisher notifier)
|
INotificationPublisher notifier,
|
||||||
: base(logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig,
|
IDryRunInterceptor dryRunInterceptor
|
||||||
cache, filenameEvaluator, striker, notifier)
|
) : base(
|
||||||
|
logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache,
|
||||||
|
filenameEvaluator, striker, notifier, dryRunInterceptor
|
||||||
|
)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Dispose() { }
|
public override void Dispose() { }
|
||||||
public override Task LoginAsync() => Task.CompletedTask;
|
public override Task LoginAsync() => Task.CompletedTask;
|
||||||
public override Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash) => Task.FromResult(new StalledResult());
|
public override Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads) => Task.FromResult(new StalledResult());
|
||||||
public override Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash, BlocklistType blocklistType,
|
public override Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash, BlocklistType blocklistType,
|
||||||
ConcurrentBag<string> patterns, ConcurrentBag<Regex> regexes) => Task.FromResult(new BlockFilesResult());
|
ConcurrentBag<string> patterns, ConcurrentBag<Regex> regexes, IReadOnlyList<string> ignoredDownloads) => Task.FromResult(new BlockFilesResult());
|
||||||
public override Task DeleteDownload(string hash) => Task.CompletedTask;
|
public override Task DeleteDownload(string hash) => Task.CompletedTask;
|
||||||
public override Task<List<object>?> GetAllDownloadsToBeCleaned(List<Category> categories) => Task.FromResult<List<object>?>(null);
|
public override Task<List<object>?> GetAllDownloadsToBeCleaned(List<Category> categories) => Task.FromResult<List<object>?>(null);
|
||||||
public override Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes) => Task.CompletedTask;
|
public override Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes,
|
||||||
|
IReadOnlyList<string> ignoredDownloads) => Task.CompletedTask;
|
||||||
|
|
||||||
// Expose protected methods for testing
|
// Expose protected methods for testing
|
||||||
public new void ResetStrikesOnProgress(string hash, long downloaded) => base.ResetStrikesOnProgress(hash, downloaded);
|
public new void ResetStrikesOnProgress(string hash, long downloaded) => base.ResetStrikesOnProgress(hash, downloaded);
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Domain.Models.Deluge.Response;
|
||||||
|
|
||||||
|
namespace Infrastructure.Extensions;
|
||||||
|
|
||||||
|
public static class DelugeExtensions
|
||||||
|
{
|
||||||
|
public static bool ShouldIgnore(this TorrentStatus download, IReadOnlyList<string> ignoredDownloads)
|
||||||
|
{
|
||||||
|
foreach (string value in ignoredDownloads)
|
||||||
|
{
|
||||||
|
if (download.Hash?.Equals(value, StringComparison.InvariantCultureIgnoreCase) ?? false)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (download.Label?.Equals(value, StringComparison.InvariantCultureIgnoreCase) ?? false)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (download.Trackers.Any(x => x.Url.Host.EndsWith(value, StringComparison.InvariantCultureIgnoreCase)))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using QBittorrent.Client;
|
||||||
|
|
||||||
|
namespace Infrastructure.Extensions;
|
||||||
|
|
||||||
|
public static class QBitExtensions
|
||||||
|
{
|
||||||
|
public static bool ShouldIgnore(this TorrentInfo download, IReadOnlyList<string> ignoredDownloads)
|
||||||
|
{
|
||||||
|
foreach (string value in ignoredDownloads)
|
||||||
|
{
|
||||||
|
if (download.Hash.Equals(value, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (download.Category.Equals(value, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (download.Tags.Contains(value, StringComparer.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ShouldIgnore(this TorrentTracker tracker, IReadOnlyList<string> ignoredDownloads)
|
||||||
|
{
|
||||||
|
foreach (string value in ignoredDownloads)
|
||||||
|
{
|
||||||
|
if (tracker.Url.Host.EndsWith(value, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using Transmission.API.RPC.Entity;
|
||||||
|
|
||||||
|
namespace Infrastructure.Extensions;
|
||||||
|
|
||||||
|
public static class TransmissionExtensions
|
||||||
|
{
|
||||||
|
public static bool ShouldIgnore(this TorrentInfo download, IReadOnlyList<string> ignoredDownloads)
|
||||||
|
{
|
||||||
|
foreach (string value in ignoredDownloads)
|
||||||
|
{
|
||||||
|
if (download.HashString?.Equals(value, StringComparison.InvariantCultureIgnoreCase) ?? false)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (download.GetCategory().Equals(value, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool? hasIgnoredTracker = download.Trackers?
|
||||||
|
.Any(x => new Uri(x.Announce!).Host.EndsWith(value, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
|
if (hasIgnoredTracker is true)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetCategory(this TorrentInfo download)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(download.DownloadDir))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.GetFileName(Path.TrimEndingDirectorySeparator(download.DownloadDir));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,4 +11,6 @@ public static class CacheKeys
|
|||||||
public static string BlocklistRegexes(InstanceType instanceType) => $"{instanceType.ToString()}_regexes";
|
public static string BlocklistRegexes(InstanceType instanceType) => $"{instanceType.ToString()}_regexes";
|
||||||
|
|
||||||
public static string Item(string hash) => $"item_{hash}";
|
public static string Item(string hash) => $"item_{hash}";
|
||||||
|
|
||||||
|
public static string IgnoredDownloads(string name) => $"{name}_ignored";
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Castle.Core.AsyncInterceptor" Version="2.1.0" />
|
|
||||||
<PackageReference Include="FLM.QBittorrent" Version="1.0.0" />
|
<PackageReference Include="FLM.QBittorrent" Version="1.0.0" />
|
||||||
<PackageReference Include="FLM.Transmission" Version="1.0.2" />
|
<PackageReference Include="FLM.Transmission" Version="1.0.2" />
|
||||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Castle.DynamicProxy;
|
|
||||||
using Common.Attributes;
|
using Common.Attributes;
|
||||||
using Common.Configuration.General;
|
using Common.Configuration.General;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -7,41 +6,70 @@ using Microsoft.Extensions.Options;
|
|||||||
|
|
||||||
namespace Infrastructure.Interceptors;
|
namespace Infrastructure.Interceptors;
|
||||||
|
|
||||||
public class DryRunAsyncInterceptor : AsyncInterceptorBase
|
public class DryRunInterceptor : IDryRunInterceptor
|
||||||
{
|
{
|
||||||
private readonly ILogger<DryRunAsyncInterceptor> _logger;
|
private readonly ILogger<DryRunInterceptor> _logger;
|
||||||
private readonly DryRunConfig _config;
|
private readonly DryRunConfig _config;
|
||||||
|
|
||||||
public DryRunAsyncInterceptor(ILogger<DryRunAsyncInterceptor> logger, IOptions<DryRunConfig> config)
|
public DryRunInterceptor(ILogger<DryRunInterceptor> logger, IOptions<DryRunConfig> config)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_config = config.Value;
|
_config = config.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task InterceptAsync(IInvocation invocation, IInvocationProceedInfo proceedInfo, Func<IInvocation, IInvocationProceedInfo, Task> proceed)
|
public void Intercept(Action action)
|
||||||
{
|
{
|
||||||
MethodInfo? method = invocation.MethodInvocationTarget ?? invocation.Method;
|
MethodInfo methodInfo = action.Method;
|
||||||
if (IsDryRun(method))
|
|
||||||
|
if (IsDryRun(methodInfo))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("[DRY RUN] skipping method: {name}", method.Name);
|
_logger.LogInformation("[DRY RUN] skipping method: {name}", methodInfo.Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await proceed(invocation, proceedInfo);
|
action();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task<TResult> InterceptAsync<TResult>(IInvocation invocation, IInvocationProceedInfo proceedInfo, Func<IInvocation, IInvocationProceedInfo, Task<TResult>> proceed)
|
public Task InterceptAsync(Delegate action, params object[] parameters)
|
||||||
{
|
{
|
||||||
MethodInfo? method = invocation.MethodInvocationTarget ?? invocation.Method;
|
MethodInfo methodInfo = action.Method;
|
||||||
if (IsDryRun(method))
|
|
||||||
|
if (IsDryRun(methodInfo))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("[DRY RUN] skipping method: {name}", method.Name);
|
_logger.LogInformation("[DRY RUN] skipping method: {name}", methodInfo.Name);
|
||||||
return default!;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await proceed(invocation, proceedInfo);
|
object? result = action.DynamicInvoke(parameters);
|
||||||
}
|
|
||||||
|
|
||||||
|
if (result is Task task)
|
||||||
|
{
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<T?> InterceptAsync<T>(Delegate action, params object[] parameters)
|
||||||
|
{
|
||||||
|
MethodInfo methodInfo = action.Method;
|
||||||
|
|
||||||
|
if (IsDryRun(methodInfo))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[DRY RUN] skipping method: {name}", methodInfo.Name);
|
||||||
|
return Task.FromResult(default(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
object? result = action.DynamicInvoke(parameters);
|
||||||
|
|
||||||
|
if (result is Task<T?> task)
|
||||||
|
{
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(default(T));
|
||||||
|
}
|
||||||
|
|
||||||
private bool IsDryRun(MethodInfo method)
|
private bool IsDryRun(MethodInfo method)
|
||||||
{
|
{
|
||||||
return method.GetCustomAttributes(typeof(DryRunSafeguardAttribute), true).Any() && _config.IsDryRun;
|
return method.GetCustomAttributes(typeof(DryRunSafeguardAttribute), true).Any() && _config.IsDryRun;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Infrastructure.Interceptors;
|
||||||
|
|
||||||
|
public interface IDryRunInterceptor
|
||||||
|
{
|
||||||
|
void Intercept(Action action);
|
||||||
|
|
||||||
|
Task InterceptAsync(Delegate action, params object[] parameters);
|
||||||
|
|
||||||
|
Task<T?> InterceptAsync<T>(Delegate action, params object[] parameters);
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
namespace Infrastructure.Interceptors;
|
|
||||||
|
|
||||||
public interface IDryRunService : IInterceptedService
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Infrastructure.Interceptors;
|
|
||||||
|
|
||||||
public interface IInterceptedService
|
|
||||||
{
|
|
||||||
public object Proxy { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
namespace Infrastructure.Interceptors;
|
|
||||||
|
|
||||||
public class InterceptedService : IInterceptedService
|
|
||||||
{
|
|
||||||
private object? _proxy;
|
|
||||||
|
|
||||||
public object Proxy
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_proxy is null)
|
|
||||||
{
|
|
||||||
throw new Exception("Proxy is not set");
|
|
||||||
}
|
|
||||||
|
|
||||||
return _proxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
set => _proxy = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using Common.Configuration;
|
||||||
|
using Infrastructure.Helpers;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Infrastructure.Providers;
|
||||||
|
|
||||||
|
public sealed class IgnoredDownloadsProvider<T>
|
||||||
|
where T : IIgnoredDownloadsConfig
|
||||||
|
{
|
||||||
|
private readonly ILogger<IgnoredDownloadsProvider<T>> _logger;
|
||||||
|
private IIgnoredDownloadsConfig _config;
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private DateTime _lastModified = DateTime.MinValue;
|
||||||
|
|
||||||
|
public IgnoredDownloadsProvider(ILogger<IgnoredDownloadsProvider<T>> logger, IOptionsMonitor<T> config, IMemoryCache cache)
|
||||||
|
{
|
||||||
|
_config = config.CurrentValue;
|
||||||
|
config.OnChange((newValue) => _config = newValue);
|
||||||
|
_logger = logger;
|
||||||
|
_cache = cache;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(_config.IgnoredDownloadsPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(_config.IgnoredDownloadsPath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("file not found", _config.IgnoredDownloadsPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<string>> GetIgnoredDownloads()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_config.IgnoredDownloadsPath))
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInfo fileInfo = new(_config.IgnoredDownloadsPath);
|
||||||
|
|
||||||
|
if (fileInfo.LastWriteTime > _lastModified ||
|
||||||
|
!_cache.TryGetValue(CacheKeys.IgnoredDownloads(typeof(T).Name), out IReadOnlyList<string>? ignoredDownloads) ||
|
||||||
|
ignoredDownloads is null)
|
||||||
|
{
|
||||||
|
_lastModified = fileInfo.LastWriteTime;
|
||||||
|
|
||||||
|
return await LoadFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ignoredDownloads;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<string>> LoadFile()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_config.IgnoredDownloadsPath))
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] ignoredDownloads = (await File.ReadAllLinesAsync(_config.IgnoredDownloadsPath))
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
_cache.Set(CacheKeys.IgnoredDownloads(typeof(T).Name), ignoredDownloads);
|
||||||
|
|
||||||
|
_logger.LogInformation("ignored downloads reloaded");
|
||||||
|
|
||||||
|
return ignoredDownloads;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogError(exception, "error while reading ignored downloads file | {file}", _config.IgnoredDownloadsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,27 +15,22 @@ using Newtonsoft.Json;
|
|||||||
|
|
||||||
namespace Infrastructure.Verticals.Arr;
|
namespace Infrastructure.Verticals.Arr;
|
||||||
|
|
||||||
public abstract class ArrClient : InterceptedService, IArrClient, IDryRunService
|
public abstract class ArrClient : IArrClient
|
||||||
{
|
{
|
||||||
protected readonly ILogger<ArrClient> _logger;
|
protected readonly ILogger<ArrClient> _logger;
|
||||||
protected readonly HttpClient _httpClient;
|
protected readonly HttpClient _httpClient;
|
||||||
protected readonly LoggingConfig _loggingConfig;
|
protected readonly LoggingConfig _loggingConfig;
|
||||||
protected readonly QueueCleanerConfig _queueCleanerConfig;
|
protected readonly QueueCleanerConfig _queueCleanerConfig;
|
||||||
protected readonly IStriker _striker;
|
protected readonly IStriker _striker;
|
||||||
|
protected readonly IDryRunInterceptor _dryRunInterceptor;
|
||||||
/// <summary>
|
|
||||||
/// Constructor to be used by interceptors.
|
|
||||||
/// </summary>
|
|
||||||
protected ArrClient()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ArrClient(
|
protected ArrClient(
|
||||||
ILogger<ArrClient> logger,
|
ILogger<ArrClient> logger,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IOptions<LoggingConfig> loggingConfig,
|
IOptions<LoggingConfig> loggingConfig,
|
||||||
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
||||||
IStriker striker
|
IStriker striker,
|
||||||
|
IDryRunInterceptor dryRunInterceptor
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -43,6 +38,7 @@ public abstract class ArrClient : InterceptedService, IArrClient, IDryRunService
|
|||||||
_loggingConfig = loggingConfig.Value;
|
_loggingConfig = loggingConfig.Value;
|
||||||
_queueCleanerConfig = queueCleanerConfig.Value;
|
_queueCleanerConfig = queueCleanerConfig.Value;
|
||||||
_striker = striker;
|
_striker = striker;
|
||||||
|
_dryRunInterceptor = dryRunInterceptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task<QueueListResponse> GetQueueItemsAsync(ArrInstance arrInstance, int page)
|
public virtual async Task<QueueListResponse> GetQueueItemsAsync(ArrInstance arrInstance, int page)
|
||||||
@@ -125,7 +121,8 @@ public abstract class ArrClient : InterceptedService, IArrClient, IDryRunService
|
|||||||
using HttpRequestMessage request = new(HttpMethod.Delete, uri);
|
using HttpRequestMessage request = new(HttpMethod.Delete, uri);
|
||||||
SetApiKey(request, arrInstance.ApiKey);
|
SetApiKey(request, arrInstance.ApiKey);
|
||||||
|
|
||||||
using var _ = await ((ArrClient)Proxy).SendRequestAsync(request);
|
HttpResponseMessage? response = await _dryRunInterceptor.InterceptAsync<HttpResponseMessage>(SendRequestAsync, request);
|
||||||
|
response?.Dispose();
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
removeFromClient
|
removeFromClient
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Common.Configuration.QueueCleaner;
|
|||||||
using Domain.Models.Arr;
|
using Domain.Models.Arr;
|
||||||
using Domain.Models.Arr.Queue;
|
using Domain.Models.Arr.Queue;
|
||||||
using Domain.Models.Lidarr;
|
using Domain.Models.Lidarr;
|
||||||
|
using Infrastructure.Interceptors;
|
||||||
using Infrastructure.Verticals.Arr.Interfaces;
|
using Infrastructure.Verticals.Arr.Interfaces;
|
||||||
using Infrastructure.Verticals.ItemStriker;
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -15,18 +16,14 @@ namespace Infrastructure.Verticals.Arr;
|
|||||||
|
|
||||||
public class LidarrClient : ArrClient, ILidarrClient
|
public class LidarrClient : ArrClient, ILidarrClient
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
|
||||||
public LidarrClient()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public LidarrClient(
|
public LidarrClient(
|
||||||
ILogger<LidarrClient> logger,
|
ILogger<LidarrClient> logger,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IOptions<LoggingConfig> loggingConfig,
|
IOptions<LoggingConfig> loggingConfig,
|
||||||
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
||||||
IStriker striker
|
IStriker striker,
|
||||||
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker)
|
IDryRunInterceptor dryRunInterceptor
|
||||||
|
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker, dryRunInterceptor)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +61,8 @@ public class LidarrClient : ArrClient, ILidarrClient
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var _ = await ((LidarrClient)Proxy).SendRequestAsync(request);
|
HttpResponseMessage? response = await _dryRunInterceptor.InterceptAsync<HttpResponseMessage>(SendRequestAsync, request);
|
||||||
|
response?.Dispose();
|
||||||
|
|
||||||
_logger.LogInformation("{log}", GetSearchLog(arrInstance.Url, command, true, logContext));
|
_logger.LogInformation("{log}", GetSearchLog(arrInstance.Url, command, true, logContext));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Common.Configuration.QueueCleaner;
|
|||||||
using Domain.Models.Arr;
|
using Domain.Models.Arr;
|
||||||
using Domain.Models.Arr.Queue;
|
using Domain.Models.Arr.Queue;
|
||||||
using Domain.Models.Radarr;
|
using Domain.Models.Radarr;
|
||||||
|
using Infrastructure.Interceptors;
|
||||||
using Infrastructure.Verticals.Arr.Interfaces;
|
using Infrastructure.Verticals.Arr.Interfaces;
|
||||||
using Infrastructure.Verticals.ItemStriker;
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -15,18 +16,14 @@ namespace Infrastructure.Verticals.Arr;
|
|||||||
|
|
||||||
public class RadarrClient : ArrClient, IRadarrClient
|
public class RadarrClient : ArrClient, IRadarrClient
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
|
||||||
public RadarrClient()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public RadarrClient(
|
public RadarrClient(
|
||||||
ILogger<ArrClient> logger,
|
ILogger<ArrClient> logger,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IOptions<LoggingConfig> loggingConfig,
|
IOptions<LoggingConfig> loggingConfig,
|
||||||
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
||||||
IStriker striker
|
IStriker striker,
|
||||||
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker)
|
IDryRunInterceptor dryRunInterceptor
|
||||||
|
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker, dryRunInterceptor)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +69,8 @@ public class RadarrClient : ArrClient, IRadarrClient
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var _ = await ((RadarrClient)Proxy).SendRequestAsync(request);
|
HttpResponseMessage? response = await _dryRunInterceptor.InterceptAsync<HttpResponseMessage>(SendRequestAsync, request);
|
||||||
|
response?.Dispose();
|
||||||
|
|
||||||
_logger.LogInformation("{log}", GetSearchLog(arrInstance.Url, command, true, logContext));
|
_logger.LogInformation("{log}", GetSearchLog(arrInstance.Url, command, true, logContext));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Common.Configuration.QueueCleaner;
|
|||||||
using Domain.Models.Arr;
|
using Domain.Models.Arr;
|
||||||
using Domain.Models.Arr.Queue;
|
using Domain.Models.Arr.Queue;
|
||||||
using Domain.Models.Sonarr;
|
using Domain.Models.Sonarr;
|
||||||
|
using Infrastructure.Interceptors;
|
||||||
using Infrastructure.Verticals.Arr.Interfaces;
|
using Infrastructure.Verticals.Arr.Interfaces;
|
||||||
using Infrastructure.Verticals.ItemStriker;
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -16,18 +17,14 @@ namespace Infrastructure.Verticals.Arr;
|
|||||||
|
|
||||||
public class SonarrClient : ArrClient, ISonarrClient
|
public class SonarrClient : ArrClient, ISonarrClient
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
|
||||||
public SonarrClient()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public SonarrClient(
|
public SonarrClient(
|
||||||
ILogger<SonarrClient> logger,
|
ILogger<SonarrClient> logger,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IOptions<LoggingConfig> loggingConfig,
|
IOptions<LoggingConfig> loggingConfig,
|
||||||
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
||||||
IStriker striker
|
IStriker striker,
|
||||||
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker)
|
IDryRunInterceptor dryRunInterceptor
|
||||||
|
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker, dryRunInterceptor)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,8 +65,9 @@ public class SonarrClient : ArrClient, ISonarrClient
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var _ = await ((SonarrClient)Proxy).SendRequestAsync(request);
|
HttpResponseMessage? response = await _dryRunInterceptor.InterceptAsync<HttpResponseMessage>(SendRequestAsync, request);
|
||||||
|
response?.Dispose();
|
||||||
|
|
||||||
_logger.LogInformation("{log}", GetSearchLog(command.SearchType, arrInstance.Url, command, true, logContext));
|
_logger.LogInformation("{log}", GetSearchLog(command.SearchType, arrInstance.Url, command, true, logContext));
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ public sealed class BlocklistProvider
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await LoadPatternsAndRegexesAsync(_sonarrConfig.Block.Type, _sonarrConfig.Block.Path, InstanceType.Sonarr);
|
await LoadPatternsAndRegexesAsync(_sonarrConfig, InstanceType.Sonarr);
|
||||||
await LoadPatternsAndRegexesAsync(_radarrConfig.Block.Type, _radarrConfig.Block.Path, InstanceType.Radarr);
|
await LoadPatternsAndRegexesAsync(_radarrConfig, InstanceType.Radarr);
|
||||||
await LoadPatternsAndRegexesAsync(_lidarrConfig.Block.Type, _lidarrConfig.Block.Path, InstanceType.Lidarr);
|
await LoadPatternsAndRegexesAsync(_lidarrConfig, InstanceType.Lidarr);
|
||||||
|
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
@@ -83,14 +83,19 @@ public sealed class BlocklistProvider
|
|||||||
return regexes ?? [];
|
return regexes ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadPatternsAndRegexesAsync(BlocklistType blocklistType, string? blocklistPath, InstanceType instanceType)
|
private async Task LoadPatternsAndRegexesAsync(ArrConfig arrConfig, InstanceType instanceType)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(blocklistPath))
|
if (!arrConfig.Enabled)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string[] filePatterns = await ReadContentAsync(blocklistPath);
|
if (string.IsNullOrEmpty(arrConfig.Block.Path))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] filePatterns = await ReadContentAsync(arrConfig.Block.Path);
|
||||||
|
|
||||||
long startTime = Stopwatch.GetTimestamp();
|
long startTime = Stopwatch.GetTimestamp();
|
||||||
ParallelOptions options = new() { MaxDegreeOfParallelism = 5 };
|
ParallelOptions options = new() { MaxDegreeOfParallelism = 5 };
|
||||||
@@ -121,13 +126,13 @@ public sealed class BlocklistProvider
|
|||||||
|
|
||||||
TimeSpan elapsed = Stopwatch.GetElapsedTime(startTime);
|
TimeSpan elapsed = Stopwatch.GetElapsedTime(startTime);
|
||||||
|
|
||||||
_cache.Set(CacheKeys.BlocklistType(instanceType), blocklistType);
|
_cache.Set(CacheKeys.BlocklistType(instanceType), arrConfig.Block.Type);
|
||||||
_cache.Set(CacheKeys.BlocklistPatterns(instanceType), patterns);
|
_cache.Set(CacheKeys.BlocklistPatterns(instanceType), patterns);
|
||||||
_cache.Set(CacheKeys.BlocklistRegexes(instanceType), regexes);
|
_cache.Set(CacheKeys.BlocklistRegexes(instanceType), 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 | {path}", elapsed.TotalMilliseconds, blocklistPath);
|
_logger.LogDebug("blocklist loaded in {elapsed} ms | {path}", elapsed.TotalMilliseconds, arrConfig.Block.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string[]> ReadContentAsync(string path)
|
private async Task<string[]> ReadContentAsync(string path)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Common.Configuration.DownloadClient;
|
|||||||
using Domain.Enums;
|
using Domain.Enums;
|
||||||
using Domain.Models.Arr;
|
using Domain.Models.Arr;
|
||||||
using Domain.Models.Arr.Queue;
|
using Domain.Models.Arr.Queue;
|
||||||
|
using Infrastructure.Providers;
|
||||||
using Infrastructure.Verticals.Arr;
|
using Infrastructure.Verticals.Arr;
|
||||||
using Infrastructure.Verticals.Arr.Interfaces;
|
using Infrastructure.Verticals.Arr.Interfaces;
|
||||||
using Infrastructure.Verticals.Context;
|
using Infrastructure.Verticals.Context;
|
||||||
@@ -22,7 +23,8 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
{
|
{
|
||||||
private readonly ContentBlockerConfig _config;
|
private readonly ContentBlockerConfig _config;
|
||||||
private readonly BlocklistProvider _blocklistProvider;
|
private readonly BlocklistProvider _blocklistProvider;
|
||||||
|
private readonly IgnoredDownloadsProvider<ContentBlockerConfig> _ignoredDownloadsProvider;
|
||||||
|
|
||||||
public ContentBlocker(
|
public ContentBlocker(
|
||||||
ILogger<ContentBlocker> logger,
|
ILogger<ContentBlocker> logger,
|
||||||
IOptions<ContentBlockerConfig> config,
|
IOptions<ContentBlockerConfig> config,
|
||||||
@@ -36,7 +38,8 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
ArrQueueIterator arrArrQueueIterator,
|
ArrQueueIterator arrArrQueueIterator,
|
||||||
BlocklistProvider blocklistProvider,
|
BlocklistProvider blocklistProvider,
|
||||||
DownloadServiceFactory downloadServiceFactory,
|
DownloadServiceFactory downloadServiceFactory,
|
||||||
NotificationPublisher notifier
|
INotificationPublisher notifier,
|
||||||
|
IgnoredDownloadsProvider<ContentBlockerConfig> ignoredDownloadsProvider
|
||||||
) : base(
|
) : base(
|
||||||
logger, downloadClientConfig,
|
logger, downloadClientConfig,
|
||||||
sonarrConfig, radarrConfig, lidarrConfig,
|
sonarrConfig, radarrConfig, lidarrConfig,
|
||||||
@@ -47,6 +50,7 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
{
|
{
|
||||||
_config = config.Value;
|
_config = config.Value;
|
||||||
_blocklistProvider = blocklistProvider;
|
_blocklistProvider = blocklistProvider;
|
||||||
|
_ignoredDownloadsProvider = ignoredDownloadsProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task ExecuteAsync()
|
public override async Task ExecuteAsync()
|
||||||
@@ -73,6 +77,8 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
|
|
||||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||||
{
|
{
|
||||||
|
IReadOnlyList<string> ignoredDownloads = await _ignoredDownloadsProvider.GetIgnoredDownloads();
|
||||||
|
|
||||||
using var _ = LogContext.PushProperty("InstanceName", instanceType.ToString());
|
using var _ = LogContext.PushProperty("InstanceName", instanceType.ToString());
|
||||||
|
|
||||||
HashSet<SearchItem> itemsToBeRefreshed = [];
|
HashSet<SearchItem> itemsToBeRefreshed = [];
|
||||||
@@ -106,13 +112,19 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ignoredDownloads.Contains(record.DownloadId, StringComparer.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("skip | {title} | ignored", record.Title);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// push record to context
|
// push record to context
|
||||||
ContextProvider.Set(nameof(QueueRecord), record);
|
ContextProvider.Set(nameof(QueueRecord), record);
|
||||||
|
|
||||||
_logger.LogDebug("searching unwanted files for {title}", record.Title);
|
_logger.LogDebug("searching unwanted files for {title}", record.Title);
|
||||||
|
|
||||||
BlockFilesResult result = await _downloadService
|
BlockFilesResult result = await _downloadService
|
||||||
.BlockUnwantedFilesAsync(record.DownloadId, blocklistType, patterns, regexes);
|
.BlockUnwantedFilesAsync(record.DownloadId, blocklistType, patterns, regexes, ignoredDownloads);
|
||||||
|
|
||||||
if (!result.ShouldRemove)
|
if (!result.ShouldRemove)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Common.Configuration.DownloadCleaner;
|
|||||||
using Common.Configuration.DownloadClient;
|
using Common.Configuration.DownloadClient;
|
||||||
using Domain.Enums;
|
using Domain.Enums;
|
||||||
using Domain.Models.Arr.Queue;
|
using Domain.Models.Arr.Queue;
|
||||||
|
using Infrastructure.Providers;
|
||||||
using Infrastructure.Verticals.Arr;
|
using Infrastructure.Verticals.Arr;
|
||||||
using Infrastructure.Verticals.Arr.Interfaces;
|
using Infrastructure.Verticals.Arr.Interfaces;
|
||||||
using Infrastructure.Verticals.DownloadClient;
|
using Infrastructure.Verticals.DownloadClient;
|
||||||
@@ -17,6 +18,7 @@ namespace Infrastructure.Verticals.DownloadCleaner;
|
|||||||
public sealed class DownloadCleaner : GenericHandler
|
public sealed class DownloadCleaner : GenericHandler
|
||||||
{
|
{
|
||||||
private readonly DownloadCleanerConfig _config;
|
private readonly DownloadCleanerConfig _config;
|
||||||
|
private readonly IgnoredDownloadsProvider<DownloadCleanerConfig> _ignoredDownloadsProvider;
|
||||||
private readonly HashSet<string> _excludedHashes = [];
|
private readonly HashSet<string> _excludedHashes = [];
|
||||||
|
|
||||||
public DownloadCleaner(
|
public DownloadCleaner(
|
||||||
@@ -31,7 +33,8 @@ public sealed class DownloadCleaner : GenericHandler
|
|||||||
LidarrClient lidarrClient,
|
LidarrClient lidarrClient,
|
||||||
ArrQueueIterator arrArrQueueIterator,
|
ArrQueueIterator arrArrQueueIterator,
|
||||||
DownloadServiceFactory downloadServiceFactory,
|
DownloadServiceFactory downloadServiceFactory,
|
||||||
NotificationPublisher notifier
|
INotificationPublisher notifier,
|
||||||
|
IgnoredDownloadsProvider<DownloadCleanerConfig> ignoredDownloadsProvider
|
||||||
) : base(
|
) : base(
|
||||||
logger, downloadClientConfig,
|
logger, downloadClientConfig,
|
||||||
sonarrConfig, radarrConfig, lidarrConfig,
|
sonarrConfig, radarrConfig, lidarrConfig,
|
||||||
@@ -42,16 +45,25 @@ public sealed class DownloadCleaner : GenericHandler
|
|||||||
{
|
{
|
||||||
_config = config.Value;
|
_config = config.Value;
|
||||||
_config.Validate();
|
_config.Validate();
|
||||||
|
_ignoredDownloadsProvider = ignoredDownloadsProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task ExecuteAsync()
|
public override async Task ExecuteAsync()
|
||||||
{
|
{
|
||||||
|
if (_downloadClientConfig.DownloadClient is Common.Enums.DownloadClient.None)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("download client is set to none");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_config.Categories?.Count is null or 0)
|
if (_config.Categories?.Count is null or 0)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("no categories configured");
|
_logger.LogWarning("no categories configured");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IReadOnlyList<string> ignoredDownloads = await _ignoredDownloadsProvider.GetIgnoredDownloads();
|
||||||
|
|
||||||
await _downloadService.LoginAsync();
|
await _downloadService.LoginAsync();
|
||||||
|
|
||||||
List<object>? downloads = await _downloadService.GetAllDownloadsToBeCleaned(_config.Categories);
|
List<object>? downloads = await _downloadService.GetAllDownloadsToBeCleaned(_config.Categories);
|
||||||
@@ -69,7 +81,7 @@ public sealed class DownloadCleaner : GenericHandler
|
|||||||
await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr, true);
|
await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr, true);
|
||||||
await ProcessArrConfigAsync(_lidarrConfig, InstanceType.Lidarr, true);
|
await ProcessArrConfigAsync(_lidarrConfig, InstanceType.Lidarr, true);
|
||||||
|
|
||||||
await _downloadService.CleanDownloads(downloads, _config.Categories, _excludedHashes);
|
await _downloadService.CleanDownloads(downloads, _config.Categories, _excludedHashes, ignoredDownloads);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||||
|
|||||||
@@ -16,6 +16,20 @@ public sealed class DelugeClient
|
|||||||
private readonly DelugeConfig _config;
|
private readonly DelugeConfig _config;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
private static readonly IReadOnlyList<string> Fields =
|
||||||
|
[
|
||||||
|
"hash",
|
||||||
|
"state",
|
||||||
|
"name",
|
||||||
|
"eta",
|
||||||
|
"private",
|
||||||
|
"total_done",
|
||||||
|
"label",
|
||||||
|
"seeding_time",
|
||||||
|
"ratio",
|
||||||
|
"trackers"
|
||||||
|
];
|
||||||
|
|
||||||
public DelugeClient(IOptions<DelugeConfig> config, IHttpClientFactory httpClientFactory)
|
public DelugeClient(IOptions<DelugeConfig> config, IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
_config = config.Value;
|
_config = config.Value;
|
||||||
@@ -68,7 +82,7 @@ public sealed class DelugeClient
|
|||||||
return await SendRequest<TorrentStatus?>(
|
return await SendRequest<TorrentStatus?>(
|
||||||
"web.get_torrent_status",
|
"web.get_torrent_status",
|
||||||
hash,
|
hash,
|
||||||
new[] { "hash", "state", "name", "eta", "private", "total_done", "label", "seeding_time", "ratio" }
|
Fields
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +91,7 @@ public sealed class DelugeClient
|
|||||||
Dictionary<string, TorrentStatus>? downloads = await SendRequest<Dictionary<string, TorrentStatus>?>(
|
Dictionary<string, TorrentStatus>? downloads = await SendRequest<Dictionary<string, TorrentStatus>?>(
|
||||||
"core.get_torrents_status",
|
"core.get_torrents_status",
|
||||||
"",
|
"",
|
||||||
new[] { "hash", "state", "name", "eta", "private", "total_done", "label", "seeding_time", "ratio" }
|
Fields
|
||||||
);
|
);
|
||||||
|
|
||||||
return downloads?.Values.ToList();
|
return downloads?.Values.ToList();
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ using Common.Configuration.DownloadClient;
|
|||||||
using Common.Configuration.QueueCleaner;
|
using Common.Configuration.QueueCleaner;
|
||||||
using Domain.Enums;
|
using Domain.Enums;
|
||||||
using Domain.Models.Deluge.Response;
|
using Domain.Models.Deluge.Response;
|
||||||
|
using Infrastructure.Extensions;
|
||||||
|
using Infrastructure.Interceptors;
|
||||||
using Infrastructure.Verticals.ContentBlocker;
|
using Infrastructure.Verticals.ContentBlocker;
|
||||||
using Infrastructure.Verticals.Context;
|
using Infrastructure.Verticals.Context;
|
||||||
using Infrastructure.Verticals.ItemStriker;
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
using Infrastructure.Verticals.Notifications;
|
using Infrastructure.Verticals.Notifications;
|
||||||
using MassTransit.Configuration;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -22,11 +23,6 @@ public class DelugeService : DownloadService, IDelugeService
|
|||||||
{
|
{
|
||||||
private readonly DelugeClient _client;
|
private readonly DelugeClient _client;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public DelugeService()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public DelugeService(
|
public DelugeService(
|
||||||
ILogger<DelugeService> logger,
|
ILogger<DelugeService> logger,
|
||||||
IOptions<DelugeConfig> config,
|
IOptions<DelugeConfig> config,
|
||||||
@@ -37,8 +33,12 @@ public class DelugeService : DownloadService, IDelugeService
|
|||||||
IMemoryCache cache,
|
IMemoryCache cache,
|
||||||
IFilenameEvaluator filenameEvaluator,
|
IFilenameEvaluator filenameEvaluator,
|
||||||
IStriker striker,
|
IStriker striker,
|
||||||
NotificationPublisher notifier
|
INotificationPublisher notifier,
|
||||||
) : base(logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache, filenameEvaluator, striker, notifier)
|
IDryRunInterceptor dryRunInterceptor
|
||||||
|
) : base(
|
||||||
|
logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache,
|
||||||
|
filenameEvaluator, striker, notifier, dryRunInterceptor
|
||||||
|
)
|
||||||
{
|
{
|
||||||
config.Value.Validate();
|
config.Value.Validate();
|
||||||
_client = new (config, httpClientFactory);
|
_client = new (config, httpClientFactory);
|
||||||
@@ -50,20 +50,26 @@ public class DelugeService : DownloadService, IDelugeService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
|
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads)
|
||||||
{
|
{
|
||||||
hash = hash.ToLowerInvariant();
|
hash = hash.ToLowerInvariant();
|
||||||
|
|
||||||
DelugeContents? contents = null;
|
DelugeContents? contents = null;
|
||||||
StalledResult result = new();
|
StalledResult result = new();
|
||||||
|
|
||||||
TorrentStatus? status = await _client.GetTorrentStatus(hash);
|
TorrentStatus? download = await _client.GetTorrentStatus(hash);
|
||||||
|
|
||||||
if (status?.Hash is null)
|
if (download?.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 result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -89,8 +95,8 @@ public class DelugeService : DownloadService, IDelugeService
|
|||||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(status);
|
result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(download);
|
||||||
result.IsPrivate = status.Private;
|
result.IsPrivate = download.Private;
|
||||||
|
|
||||||
if (!shouldRemove && result.ShouldRemove)
|
if (!shouldRemove && result.ShouldRemove)
|
||||||
{
|
{
|
||||||
@@ -101,30 +107,37 @@ public class DelugeService : DownloadService, IDelugeService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash,
|
||||||
string hash,
|
|
||||||
BlocklistType blocklistType,
|
BlocklistType blocklistType,
|
||||||
ConcurrentBag<string> patterns,
|
ConcurrentBag<string> patterns,
|
||||||
ConcurrentBag<Regex> regexes
|
ConcurrentBag<Regex> regexes, IReadOnlyList<string> ignoredDownloads)
|
||||||
)
|
|
||||||
{
|
{
|
||||||
hash = hash.ToLowerInvariant();
|
hash = hash.ToLowerInvariant();
|
||||||
|
|
||||||
TorrentStatus? status = await _client.GetTorrentStatus(hash);
|
TorrentStatus? download = await _client.GetTorrentStatus(hash);
|
||||||
BlockFilesResult result = new();
|
BlockFilesResult result = new();
|
||||||
|
|
||||||
if (status?.Hash is null)
|
if (download?.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 result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.IsPrivate = status.Private;
|
|
||||||
|
|
||||||
if (_contentBlockerConfig.IgnorePrivate && status.Private)
|
var ceva = await _client.GetTorrentExtended(hash);
|
||||||
|
|
||||||
|
|
||||||
|
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.IsPrivate = download.Private;
|
||||||
|
|
||||||
|
if (_contentBlockerConfig.IgnorePrivate && download.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}", download.Name);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +203,7 @@ public class DelugeService : DownloadService, IDelugeService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ((DelugeService)Proxy).ChangeFilesPriority(hash, sortedPriorities);
|
await _dryRunInterceptor.InterceptAsync(ChangeFilesPriority, hash, sortedPriorities);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -206,7 +219,8 @@ public class DelugeService : DownloadService, IDelugeService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes)
|
public override async Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes,
|
||||||
|
IReadOnlyList<string> ignoredDownloads)
|
||||||
{
|
{
|
||||||
foreach (TorrentStatus download in downloads)
|
foreach (TorrentStatus download in downloads)
|
||||||
{
|
{
|
||||||
@@ -214,6 +228,12 @@ public class DelugeService : DownloadService, IDelugeService
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
Category? category = categoriesToClean
|
Category? category = categoriesToClean
|
||||||
.FirstOrDefault(x => x.Name.Equals(download.Label, StringComparison.InvariantCultureIgnoreCase));
|
.FirstOrDefault(x => x.Name.Equals(download.Label, StringComparison.InvariantCultureIgnoreCase));
|
||||||
@@ -245,8 +265,8 @@ public class DelugeService : DownloadService, IDelugeService
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ((DelugeService)Proxy).DeleteDownload(download.Hash);
|
await _dryRunInterceptor.InterceptAsync(DeleteDownload, download.Hash);
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"download cleaned | {reason} reached | {name}",
|
"download cleaned | {reason} reached | {name}",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ using Microsoft.Extensions.Options;
|
|||||||
|
|
||||||
namespace Infrastructure.Verticals.DownloadClient;
|
namespace Infrastructure.Verticals.DownloadClient;
|
||||||
|
|
||||||
public abstract class DownloadService : InterceptedService, IDownloadService
|
public abstract class DownloadService : IDownloadService
|
||||||
{
|
{
|
||||||
protected readonly ILogger<DownloadService> _logger;
|
protected readonly ILogger<DownloadService> _logger;
|
||||||
protected readonly QueueCleanerConfig _queueCleanerConfig;
|
protected readonly QueueCleanerConfig _queueCleanerConfig;
|
||||||
@@ -28,15 +28,9 @@ public abstract class DownloadService : InterceptedService, IDownloadService
|
|||||||
protected readonly IFilenameEvaluator _filenameEvaluator;
|
protected readonly IFilenameEvaluator _filenameEvaluator;
|
||||||
protected readonly IStriker _striker;
|
protected readonly IStriker _striker;
|
||||||
protected readonly MemoryCacheEntryOptions _cacheOptions;
|
protected readonly MemoryCacheEntryOptions _cacheOptions;
|
||||||
protected readonly NotificationPublisher _notifier;
|
protected readonly INotificationPublisher _notifier;
|
||||||
|
protected readonly IDryRunInterceptor _dryRunInterceptor;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor to be used by interceptors.
|
|
||||||
/// </summary>
|
|
||||||
protected DownloadService()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected DownloadService(
|
protected DownloadService(
|
||||||
ILogger<DownloadService> logger,
|
ILogger<DownloadService> logger,
|
||||||
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
||||||
@@ -45,7 +39,9 @@ public abstract class DownloadService : InterceptedService, IDownloadService
|
|||||||
IMemoryCache cache,
|
IMemoryCache cache,
|
||||||
IFilenameEvaluator filenameEvaluator,
|
IFilenameEvaluator filenameEvaluator,
|
||||||
IStriker striker,
|
IStriker striker,
|
||||||
NotificationPublisher notifier)
|
INotificationPublisher notifier,
|
||||||
|
IDryRunInterceptor dryRunInterceptor
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_queueCleanerConfig = queueCleanerConfig.Value;
|
_queueCleanerConfig = queueCleanerConfig.Value;
|
||||||
@@ -55,6 +51,7 @@ public abstract class DownloadService : InterceptedService, IDownloadService
|
|||||||
_filenameEvaluator = filenameEvaluator;
|
_filenameEvaluator = filenameEvaluator;
|
||||||
_striker = striker;
|
_striker = striker;
|
||||||
_notifier = notifier;
|
_notifier = notifier;
|
||||||
|
_dryRunInterceptor = dryRunInterceptor;
|
||||||
_cacheOptions = new MemoryCacheEntryOptions()
|
_cacheOptions = new MemoryCacheEntryOptions()
|
||||||
.SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer);
|
.SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer);
|
||||||
}
|
}
|
||||||
@@ -63,15 +60,13 @@ public abstract class DownloadService : InterceptedService, IDownloadService
|
|||||||
|
|
||||||
public abstract Task LoginAsync();
|
public abstract Task LoginAsync();
|
||||||
|
|
||||||
public abstract Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash);
|
public abstract Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public abstract Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
public abstract Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash,
|
||||||
string hash,
|
|
||||||
BlocklistType blocklistType,
|
BlocklistType blocklistType,
|
||||||
ConcurrentBag<string> patterns,
|
ConcurrentBag<string> patterns,
|
||||||
ConcurrentBag<Regex> regexes
|
ConcurrentBag<Regex> regexes, IReadOnlyList<string> ignoredDownloads);
|
||||||
);
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public abstract Task DeleteDownload(string hash);
|
public abstract Task DeleteDownload(string hash);
|
||||||
@@ -80,7 +75,8 @@ public abstract class DownloadService : InterceptedService, IDownloadService
|
|||||||
public abstract Task<List<object>?> GetAllDownloadsToBeCleaned(List<Category> categories);
|
public abstract Task<List<object>?> GetAllDownloadsToBeCleaned(List<Category> categories);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public abstract Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes);
|
public abstract Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes,
|
||||||
|
IReadOnlyList<string> ignoredDownloads);
|
||||||
|
|
||||||
protected void ResetStrikesOnProgress(string hash, long downloaded)
|
protected void ResetStrikesOnProgress(string hash, long downloaded)
|
||||||
{
|
{
|
||||||
@@ -134,7 +130,7 @@ public abstract class DownloadService : InterceptedService, IDownloadService
|
|||||||
|
|
||||||
return new();
|
return new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool DownloadReachedRatio(double ratio, TimeSpan seedingTime, Category category)
|
private bool DownloadReachedRatio(double ratio, TimeSpan seedingTime, Category category)
|
||||||
{
|
{
|
||||||
if (category.MaxRatio < 0)
|
if (category.MaxRatio < 0)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Text.RegularExpressions;
|
|||||||
using Common.Configuration.ContentBlocker;
|
using Common.Configuration.ContentBlocker;
|
||||||
using Common.Configuration.DownloadCleaner;
|
using Common.Configuration.DownloadCleaner;
|
||||||
using Common.Configuration.QueueCleaner;
|
using Common.Configuration.QueueCleaner;
|
||||||
|
using Infrastructure.Interceptors;
|
||||||
using Infrastructure.Verticals.ContentBlocker;
|
using Infrastructure.Verticals.ContentBlocker;
|
||||||
using Infrastructure.Verticals.ItemStriker;
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
using Infrastructure.Verticals.Notifications;
|
using Infrastructure.Verticals.Notifications;
|
||||||
@@ -14,12 +15,7 @@ namespace Infrastructure.Verticals.DownloadClient;
|
|||||||
|
|
||||||
public class DummyDownloadService : DownloadService
|
public class DummyDownloadService : DownloadService
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
public DummyDownloadService(ILogger<DownloadService> logger, IOptions<QueueCleanerConfig> queueCleanerConfig, IOptions<ContentBlockerConfig> contentBlockerConfig, IOptions<DownloadCleanerConfig> downloadCleanerConfig, IMemoryCache cache, IFilenameEvaluator filenameEvaluator, IStriker striker, INotificationPublisher notifier, IDryRunInterceptor dryRunInterceptor) : base(logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache, filenameEvaluator, striker, notifier, dryRunInterceptor)
|
||||||
public DummyDownloadService()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public DummyDownloadService(ILogger<DownloadService> logger, IOptions<QueueCleanerConfig> queueCleanerConfig, IOptions<ContentBlockerConfig> contentBlockerConfig, IOptions<DownloadCleanerConfig> downloadCleanerConfig, IMemoryCache cache, IFilenameEvaluator filenameEvaluator, IStriker striker, NotificationPublisher notifier) : base(logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache, filenameEvaluator, striker, notifier)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,12 +28,13 @@ public class DummyDownloadService : DownloadService
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
|
public override Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash, BlocklistType blocklistType, ConcurrentBag<string> patterns, ConcurrentBag<Regex> regexes)
|
public override Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash, BlocklistType blocklistType, ConcurrentBag<string> patterns,
|
||||||
|
ConcurrentBag<Regex> regexes, IReadOnlyList<string> ignoredDownloads)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -47,7 +44,8 @@ public class DummyDownloadService : DownloadService
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes)
|
public override Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes,
|
||||||
|
IReadOnlyList<string> ignoredDownloads)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using Infrastructure.Interceptors;
|
|||||||
|
|
||||||
namespace Infrastructure.Verticals.DownloadClient;
|
namespace Infrastructure.Verticals.DownloadClient;
|
||||||
|
|
||||||
public interface IDownloadService : IDisposable, IDryRunService
|
public interface IDownloadService : IDisposable
|
||||||
{
|
{
|
||||||
public Task LoginAsync();
|
public Task LoginAsync();
|
||||||
|
|
||||||
@@ -14,7 +14,8 @@ public interface IDownloadService : IDisposable, IDryRunService
|
|||||||
/// Checks whether the download should be removed from the *arr queue.
|
/// Checks whether the download should be removed from the *arr queue.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hash">The download hash.</param>
|
/// <param name="hash">The download hash.</param>
|
||||||
public Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash);
|
/// <param name="ignoredDownloads">Downloads to ignore from processing.</param>
|
||||||
|
public Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Blocks unwanted files from being fully downloaded.
|
/// Blocks unwanted files from being fully downloaded.
|
||||||
@@ -23,12 +24,13 @@ public interface IDownloadService : IDisposable, IDryRunService
|
|||||||
/// <param name="blocklistType">The <see cref="BlocklistType"/>.</param>
|
/// <param name="blocklistType">The <see cref="BlocklistType"/>.</param>
|
||||||
/// <param name="patterns">The patterns to test the files against.</param>
|
/// <param name="patterns">The patterns to test the files against.</param>
|
||||||
/// <param name="regexes">The regexes to test the files against.</param>
|
/// <param name="regexes">The regexes to test the files against.</param>
|
||||||
|
/// <param name="ignoredDownloads">Downloads to ignore from processing.</param>
|
||||||
/// <returns>True if all files have been blocked; otherwise false.</returns>
|
/// <returns>True if all files have been blocked; otherwise false.</returns>
|
||||||
public Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
public Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash,
|
||||||
string hash,
|
|
||||||
BlocklistType blocklistType,
|
BlocklistType blocklistType,
|
||||||
ConcurrentBag<string> patterns,
|
ConcurrentBag<string> patterns,
|
||||||
ConcurrentBag<Regex> regexes
|
ConcurrentBag<Regex> regexes,
|
||||||
|
IReadOnlyList<string> ignoredDownloads
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -37,14 +39,16 @@ public interface IDownloadService : IDisposable, IDryRunService
|
|||||||
/// <param name="categories">The categories by which to filter the downloads.</param>
|
/// <param name="categories">The categories by which to filter the downloads.</param>
|
||||||
/// <returns>A list of downloads for the provided categories.</returns>
|
/// <returns>A list of downloads for the provided categories.</returns>
|
||||||
Task<List<object>?> GetAllDownloadsToBeCleaned(List<Category> categories);
|
Task<List<object>?> GetAllDownloadsToBeCleaned(List<Category> categories);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cleans the downloads.
|
/// Cleans the downloads.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="downloads"></param>
|
/// <param name="downloads"></param>
|
||||||
/// <param name="categoriesToClean">The categories that should be cleaned.</param>
|
/// <param name="categoriesToClean">The categories that should be cleaned.</param>
|
||||||
/// <param name="excludedHashes">The hashes that should not be cleaned.</param>
|
/// <param name="excludedHashes">The hashes that should not be cleaned.</param>
|
||||||
public abstract Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes);
|
/// <param name="ignoredDownloads">Downloads to ignore from processing.</param>
|
||||||
|
public abstract Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes,
|
||||||
|
IReadOnlyList<string> ignoredDownloads);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes a download item.
|
/// Deletes a download item.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
namespace Infrastructure.Verticals.DownloadClient.QBittorrent;
|
namespace Infrastructure.Verticals.DownloadClient.QBittorrent;
|
||||||
|
|
||||||
public interface IQBitService : IDownloadService
|
public interface IQBitService : IDownloadService, IDisposable
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@ using Common.Configuration.DownloadClient;
|
|||||||
using Common.Configuration.QueueCleaner;
|
using Common.Configuration.QueueCleaner;
|
||||||
using Common.Helpers;
|
using Common.Helpers;
|
||||||
using Domain.Enums;
|
using Domain.Enums;
|
||||||
|
using Infrastructure.Extensions;
|
||||||
|
using Infrastructure.Interceptors;
|
||||||
using Infrastructure.Verticals.ContentBlocker;
|
using Infrastructure.Verticals.ContentBlocker;
|
||||||
using Infrastructure.Verticals.Context;
|
using Infrastructure.Verticals.Context;
|
||||||
using Infrastructure.Verticals.ItemStriker;
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
@@ -24,11 +26,6 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
private readonly QBitConfig _config;
|
private readonly QBitConfig _config;
|
||||||
private readonly QBittorrentClient _client;
|
private readonly QBittorrentClient _client;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public QBitService()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public QBitService(
|
public QBitService(
|
||||||
ILogger<QBitService> logger,
|
ILogger<QBitService> logger,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
@@ -39,8 +36,12 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
IMemoryCache cache,
|
IMemoryCache cache,
|
||||||
IFilenameEvaluator filenameEvaluator,
|
IFilenameEvaluator filenameEvaluator,
|
||||||
IStriker striker,
|
IStriker striker,
|
||||||
NotificationPublisher notifier
|
INotificationPublisher notifier,
|
||||||
) : base(logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache, filenameEvaluator, striker, notifier)
|
IDryRunInterceptor dryRunInterceptor
|
||||||
|
) : base(
|
||||||
|
logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache,
|
||||||
|
filenameEvaluator, striker, notifier, dryRunInterceptor
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_config = config.Value;
|
_config = config.Value;
|
||||||
_config.Validate();
|
_config.Validate();
|
||||||
@@ -58,18 +59,27 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
|
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads)
|
||||||
{
|
{
|
||||||
StalledResult result = new();
|
StalledResult result = new();
|
||||||
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
TorrentInfo? download = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (torrent is null)
|
if (download 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 result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IReadOnlyList<TorrentTracker> trackers = await GetTrackersAsync(hash);
|
||||||
|
|
||||||
|
if (ignoredDownloads.Count > 0 &&
|
||||||
|
(download.ShouldIgnore(ignoredDownloads) || trackers.Any(x => x.ShouldIgnore(ignoredDownloads)) is true))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash);
|
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash);
|
||||||
|
|
||||||
if (torrentProperties is null)
|
if (torrentProperties is null)
|
||||||
@@ -83,7 +93,7 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
&& boolValue;
|
&& boolValue;
|
||||||
|
|
||||||
// if all files were blocked by qBittorrent
|
// if all files were blocked by qBittorrent
|
||||||
if (torrent is { CompletionOn: not null, Downloaded: null or 0 })
|
if (download is { CompletionOn: not null, Downloaded: null or 0 })
|
||||||
{
|
{
|
||||||
result.ShouldRemove = true;
|
result.ShouldRemove = true;
|
||||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
||||||
@@ -100,7 +110,7 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.ShouldRemove = await IsItemStuckAndShouldRemove(torrent, result.IsPrivate);
|
result.ShouldRemove = await IsItemStuckAndShouldRemove(download, result.IsPrivate);
|
||||||
|
|
||||||
if (result.ShouldRemove)
|
if (result.ShouldRemove)
|
||||||
{
|
{
|
||||||
@@ -111,23 +121,32 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash,
|
||||||
string hash,
|
|
||||||
BlocklistType blocklistType,
|
BlocklistType blocklistType,
|
||||||
ConcurrentBag<string> patterns,
|
ConcurrentBag<string> patterns,
|
||||||
ConcurrentBag<Regex> regexes
|
ConcurrentBag<Regex> regexes,
|
||||||
|
IReadOnlyList<string> ignoredDownloads
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
TorrentInfo? download = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
BlockFilesResult result = new();
|
BlockFilesResult result = new();
|
||||||
|
|
||||||
if (torrent is null)
|
if (download 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 result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IReadOnlyList<TorrentTracker> trackers = await GetTrackersAsync(hash);
|
||||||
|
|
||||||
|
if (ignoredDownloads.Count > 0 &&
|
||||||
|
(download.ShouldIgnore(ignoredDownloads) || trackers.Any(x => x.ShouldIgnore(ignoredDownloads)) is true))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash);
|
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash);
|
||||||
|
|
||||||
if (torrentProperties is null)
|
if (torrentProperties is null)
|
||||||
@@ -145,7 +164,7 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
if (_contentBlockerConfig.IgnorePrivate && 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}", download.Name);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +219,7 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
|
|
||||||
foreach (int fileIndex in unwantedFiles)
|
foreach (int fileIndex in unwantedFiles)
|
||||||
{
|
{
|
||||||
await ((QBitService)Proxy).SkipFile(hash, fileIndex);
|
await _dryRunInterceptor.InterceptAsync(SkipFile, hash, fileIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -218,7 +237,8 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes)
|
public override async Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes,
|
||||||
|
IReadOnlyList<string> ignoredDownloads)
|
||||||
{
|
{
|
||||||
foreach (TorrentInfo download in downloads)
|
foreach (TorrentInfo download in downloads)
|
||||||
{
|
{
|
||||||
@@ -227,6 +247,15 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IReadOnlyList<TorrentTracker> trackers = await GetTrackersAsync(download.Hash);
|
||||||
|
|
||||||
|
if (ignoredDownloads.Count > 0 &&
|
||||||
|
(download.ShouldIgnore(ignoredDownloads) || trackers.Any(x => x.ShouldIgnore(ignoredDownloads)) is true))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
Category? category = categoriesToClean
|
Category? category = categoriesToClean
|
||||||
.FirstOrDefault(x => download.Category.Equals(x.Name, StringComparison.InvariantCultureIgnoreCase));
|
.FirstOrDefault(x => download.Category.Equals(x.Name, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
@@ -272,7 +301,7 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ((QBitService)Proxy).DeleteDownload(download.Hash);
|
await _dryRunInterceptor.InterceptAsync(DeleteDownload, download.Hash);
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"download cleaned | {reason} reached | {name}",
|
"download cleaned | {reason} reached | {name}",
|
||||||
@@ -303,7 +332,7 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
{
|
{
|
||||||
_client.Dispose();
|
_client.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> IsItemStuckAndShouldRemove(TorrentInfo torrent, bool isPrivate)
|
private async Task<bool> IsItemStuckAndShouldRemove(TorrentInfo torrent, bool isPrivate)
|
||||||
{
|
{
|
||||||
if (_queueCleanerConfig.StalledMaxStrikes is 0)
|
if (_queueCleanerConfig.StalledMaxStrikes is 0)
|
||||||
@@ -329,4 +358,11 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
|
|
||||||
return await StrikeAndCheckLimit(torrent.Hash, torrent.Name);
|
return await StrikeAndCheckLimit(torrent.Hash, torrent.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<TorrentTracker>> GetTrackersAsync(string hash)
|
||||||
|
{
|
||||||
|
return (await _client.GetTorrentTrackersAsync(hash))
|
||||||
|
.Where(x => !x.Url.ToString().Contains("**"))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@ using Common.Configuration.DownloadClient;
|
|||||||
using Common.Configuration.QueueCleaner;
|
using Common.Configuration.QueueCleaner;
|
||||||
using Common.Helpers;
|
using Common.Helpers;
|
||||||
using Domain.Enums;
|
using Domain.Enums;
|
||||||
|
using Infrastructure.Extensions;
|
||||||
|
using Infrastructure.Interceptors;
|
||||||
using Infrastructure.Verticals.ContentBlocker;
|
using Infrastructure.Verticals.ContentBlocker;
|
||||||
using Infrastructure.Verticals.Context;
|
using Infrastructure.Verticals.Context;
|
||||||
using Infrastructure.Verticals.ItemStriker;
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
@@ -26,11 +28,23 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
private readonly Client _client;
|
private readonly Client _client;
|
||||||
private TorrentInfo[]? _torrentsCache;
|
private TorrentInfo[]? _torrentsCache;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
private static readonly string[] Fields =
|
||||||
public TransmissionService()
|
[
|
||||||
{
|
TorrentFields.FILES,
|
||||||
}
|
TorrentFields.FILE_STATS,
|
||||||
|
TorrentFields.HASH_STRING,
|
||||||
|
TorrentFields.ID,
|
||||||
|
TorrentFields.ETA,
|
||||||
|
TorrentFields.NAME,
|
||||||
|
TorrentFields.STATUS,
|
||||||
|
TorrentFields.IS_PRIVATE,
|
||||||
|
TorrentFields.DOWNLOADED_EVER,
|
||||||
|
TorrentFields.DOWNLOAD_DIR,
|
||||||
|
TorrentFields.SECONDS_SEEDING,
|
||||||
|
TorrentFields.UPLOAD_RATIO,
|
||||||
|
TorrentFields.TRACKERS
|
||||||
|
];
|
||||||
|
|
||||||
public TransmissionService(
|
public TransmissionService(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
ILogger<TransmissionService> logger,
|
ILogger<TransmissionService> logger,
|
||||||
@@ -41,8 +55,12 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
IMemoryCache cache,
|
IMemoryCache cache,
|
||||||
IFilenameEvaluator filenameEvaluator,
|
IFilenameEvaluator filenameEvaluator,
|
||||||
IStriker striker,
|
IStriker striker,
|
||||||
NotificationPublisher notifier
|
INotificationPublisher notifier,
|
||||||
) : base(logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache, filenameEvaluator, striker, notifier)
|
IDryRunInterceptor dryRunInterceptor
|
||||||
|
) : base(
|
||||||
|
logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache,
|
||||||
|
filenameEvaluator, striker, notifier, dryRunInterceptor
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_config = config.Value;
|
_config = config.Value;
|
||||||
_config.Validate();
|
_config.Validate();
|
||||||
@@ -60,21 +78,27 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
|
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads)
|
||||||
{
|
{
|
||||||
StalledResult result = new();
|
StalledResult result = new();
|
||||||
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
TorrentInfo? download = await GetTorrentAsync(hash);
|
||||||
|
|
||||||
if (torrent is null)
|
if (download 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 result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool shouldRemove = torrent.FileStats?.Length > 0;
|
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||||
result.IsPrivate = torrent.IsPrivate ?? false;
|
{
|
||||||
|
_logger.LogDebug("skip | download is ignored | {name}", download.Name);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldRemove = download.FileStats?.Length > 0;
|
||||||
|
result.IsPrivate = download.IsPrivate ?? false;
|
||||||
|
|
||||||
foreach (TransmissionTorrentFileStats? stats in torrent.FileStats ?? [])
|
foreach (TransmissionTorrentFileStats? stats in download.FileStats ?? [])
|
||||||
{
|
{
|
||||||
if (!stats.Wanted.HasValue)
|
if (!stats.Wanted.HasValue)
|
||||||
{
|
{
|
||||||
@@ -95,7 +119,7 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove if all files are unwanted or download is stuck
|
// remove if all files are unwanted or download is stuck
|
||||||
result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(torrent);
|
result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(download);
|
||||||
|
|
||||||
if (!shouldRemove && result.ShouldRemove)
|
if (!shouldRemove && result.ShouldRemove)
|
||||||
{
|
{
|
||||||
@@ -106,28 +130,32 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash,
|
||||||
string hash,
|
|
||||||
BlocklistType blocklistType,
|
BlocklistType blocklistType,
|
||||||
ConcurrentBag<string> patterns,
|
ConcurrentBag<string> patterns,
|
||||||
ConcurrentBag<Regex> regexes
|
ConcurrentBag<Regex> regexes, IReadOnlyList<string> ignoredDownloads)
|
||||||
)
|
|
||||||
{
|
{
|
||||||
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
TorrentInfo? download = await GetTorrentAsync(hash);
|
||||||
BlockFilesResult result = new();
|
BlockFilesResult result = new();
|
||||||
|
|
||||||
if (torrent?.FileStats is null || torrent.Files is null)
|
if (download?.FileStats is null || download.Files is null)
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("skip | download is ignored | {name}", download.Name);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
bool isPrivate = torrent.IsPrivate ?? false;
|
bool isPrivate = download.IsPrivate ?? false;
|
||||||
result.IsPrivate = isPrivate;
|
result.IsPrivate = isPrivate;
|
||||||
|
|
||||||
if (_contentBlockerConfig.IgnorePrivate && 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}", download.Name);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,27 +163,27 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
long totalFiles = 0;
|
long totalFiles = 0;
|
||||||
long totalUnwantedFiles = 0;
|
long totalUnwantedFiles = 0;
|
||||||
|
|
||||||
for (int i = 0; i < torrent.Files.Length; i++)
|
for (int i = 0; i < download.Files.Length; i++)
|
||||||
{
|
{
|
||||||
if (torrent.FileStats?[i].Wanted == null)
|
if (download.FileStats?[i].Wanted == null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalFiles++;
|
totalFiles++;
|
||||||
|
|
||||||
if (!torrent.FileStats[i].Wanted.Value)
|
if (!download.FileStats[i].Wanted.Value)
|
||||||
{
|
{
|
||||||
totalUnwantedFiles++;
|
totalUnwantedFiles++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_filenameEvaluator.IsValid(torrent.Files[i].Name, blocklistType, patterns, regexes))
|
if (_filenameEvaluator.IsValid(download.Files[i].Name, blocklistType, patterns, regexes))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("unwanted file found | {file}", torrent.Files[i].Name);
|
_logger.LogInformation("unwanted file found | {file}", download.Files[i].Name);
|
||||||
unwantedFiles.Add(i);
|
unwantedFiles.Add(i);
|
||||||
totalUnwantedFiles++;
|
totalUnwantedFiles++;
|
||||||
}
|
}
|
||||||
@@ -174,8 +202,8 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("changing priorities | torrent {hash}", hash);
|
_logger.LogDebug("changing priorities | torrent {hash}", hash);
|
||||||
|
|
||||||
await ((TransmissionService)Proxy).SetUnwantedFiles(torrent.Id, unwantedFiles.ToArray());
|
await _dryRunInterceptor.InterceptAsync(SetUnwantedFiles, download.Id, unwantedFiles.ToArray());
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -183,34 +211,29 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task<List<object>?> GetAllDownloadsToBeCleaned(List<Category> categories)
|
public override async Task<List<object>?> GetAllDownloadsToBeCleaned(List<Category> categories)
|
||||||
{
|
{
|
||||||
string[] fields = [
|
return (await _client.TorrentGetAsync(Fields))
|
||||||
TorrentFields.FILES,
|
|
||||||
TorrentFields.FILE_STATS,
|
|
||||||
TorrentFields.HASH_STRING,
|
|
||||||
TorrentFields.ID,
|
|
||||||
TorrentFields.ETA,
|
|
||||||
TorrentFields.NAME,
|
|
||||||
TorrentFields.STATUS,
|
|
||||||
TorrentFields.IS_PRIVATE,
|
|
||||||
TorrentFields.DOWNLOADED_EVER,
|
|
||||||
TorrentFields.DOWNLOAD_DIR,
|
|
||||||
TorrentFields.SECONDS_SEEDING,
|
|
||||||
TorrentFields.UPLOAD_RATIO
|
|
||||||
];
|
|
||||||
|
|
||||||
return (await _client.TorrentGetAsync(fields))
|
|
||||||
?.Torrents
|
?.Torrents
|
||||||
?.Where(x => !string.IsNullOrEmpty(x.HashString))
|
?.Where(x => !string.IsNullOrEmpty(x.HashString))
|
||||||
.Where(x => x.Status is 5 or 6)
|
.Where(x => x.Status is 5 or 6)
|
||||||
.Where(x => categories
|
.Where(x => categories
|
||||||
.Any(cat => x.DownloadDir?.EndsWith(cat.Name, StringComparison.InvariantCultureIgnoreCase) is true)
|
.Any(cat =>
|
||||||
|
{
|
||||||
|
if (x.DownloadDir is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.GetFileName(Path.TrimEndingDirectorySeparator(x.DownloadDir))
|
||||||
|
.Equals(cat.Name, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
})
|
||||||
)
|
)
|
||||||
.Cast<object>()
|
.Cast<object>()
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes)
|
public override async Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes,
|
||||||
|
IReadOnlyList<string> ignoredDownloads)
|
||||||
{
|
{
|
||||||
foreach (TorrentInfo download in downloads)
|
foreach (TorrentInfo download in downloads)
|
||||||
{
|
{
|
||||||
@@ -218,10 +241,25 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("skip | download is ignored | {name}", download.Name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
Category? category = categoriesToClean
|
Category? category = categoriesToClean
|
||||||
.FirstOrDefault(x => download.DownloadDir?.EndsWith(x.Name, StringComparison.InvariantCultureIgnoreCase) is true);
|
.FirstOrDefault(x =>
|
||||||
|
{
|
||||||
|
if (download.DownloadDir is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.GetFileName(Path.TrimEndingDirectorySeparator(download.DownloadDir))
|
||||||
|
.Equals(x.Name, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
});
|
||||||
|
|
||||||
if (category is null)
|
if (category is null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -250,7 +288,7 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ((TransmissionService)Proxy).RemoveDownloadAsync(download.Id);
|
await _dryRunInterceptor.InterceptAsync(RemoveDownloadAsync, download.Id);
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"download cleaned | {reason} reached | {name}",
|
"download cleaned | {reason} reached | {name}",
|
||||||
@@ -333,20 +371,8 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
|
|
||||||
if (_torrentsCache is null || torrent is null)
|
if (_torrentsCache is null || torrent is null)
|
||||||
{
|
{
|
||||||
string[] fields = [
|
|
||||||
TorrentFields.FILES,
|
|
||||||
TorrentFields.FILE_STATS,
|
|
||||||
TorrentFields.HASH_STRING,
|
|
||||||
TorrentFields.ID,
|
|
||||||
TorrentFields.ETA,
|
|
||||||
TorrentFields.NAME,
|
|
||||||
TorrentFields.STATUS,
|
|
||||||
TorrentFields.IS_PRIVATE,
|
|
||||||
TorrentFields.DOWNLOADED_EVER
|
|
||||||
];
|
|
||||||
|
|
||||||
// refresh cache
|
// refresh cache
|
||||||
_torrentsCache = (await _client.TorrentGetAsync(fields))
|
_torrentsCache = (await _client.TorrentGetAsync(Fields))
|
||||||
?.Torrents;
|
?.Torrents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Common.Helpers;
|
using Common.Helpers;
|
||||||
using Domain.Enums;
|
using Domain.Enums;
|
||||||
using Infrastructure.Helpers;
|
using Infrastructure.Helpers;
|
||||||
|
using Infrastructure.Interceptors;
|
||||||
using Infrastructure.Verticals.Context;
|
using Infrastructure.Verticals.Context;
|
||||||
using Infrastructure.Verticals.Notifications;
|
using Infrastructure.Verticals.Notifications;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
@@ -13,13 +14,15 @@ public sealed class Striker : IStriker
|
|||||||
private readonly ILogger<Striker> _logger;
|
private readonly ILogger<Striker> _logger;
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
private readonly MemoryCacheEntryOptions _cacheOptions;
|
private readonly MemoryCacheEntryOptions _cacheOptions;
|
||||||
private readonly NotificationPublisher _notifier;
|
private readonly INotificationPublisher _notifier;
|
||||||
|
private readonly IDryRunInterceptor _dryRunInterceptor;
|
||||||
|
|
||||||
public Striker(ILogger<Striker> logger, IMemoryCache cache, NotificationPublisher notifier)
|
public Striker(ILogger<Striker> logger, IMemoryCache cache, INotificationPublisher notifier, IDryRunInterceptor dryRunInterceptor)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_notifier = notifier;
|
_notifier = notifier;
|
||||||
|
_dryRunInterceptor = dryRunInterceptor;
|
||||||
_cacheOptions = new MemoryCacheEntryOptions()
|
_cacheOptions = new MemoryCacheEntryOptions()
|
||||||
.SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer);
|
.SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public abstract class GenericHandler : IHandler, IDisposable
|
|||||||
protected readonly ILidarrClient _lidarrClient;
|
protected readonly ILidarrClient _lidarrClient;
|
||||||
protected readonly ArrQueueIterator _arrArrQueueIterator;
|
protected readonly ArrQueueIterator _arrArrQueueIterator;
|
||||||
protected readonly IDownloadService _downloadService;
|
protected readonly IDownloadService _downloadService;
|
||||||
protected readonly NotificationPublisher _notifier;
|
protected readonly INotificationPublisher _notifier;
|
||||||
|
|
||||||
protected GenericHandler(
|
protected GenericHandler(
|
||||||
ILogger<GenericHandler> logger,
|
ILogger<GenericHandler> logger,
|
||||||
@@ -37,7 +37,7 @@ public abstract class GenericHandler : IHandler, IDisposable
|
|||||||
ILidarrClient lidarrClient,
|
ILidarrClient lidarrClient,
|
||||||
ArrQueueIterator arrArrQueueIterator,
|
ArrQueueIterator arrArrQueueIterator,
|
||||||
DownloadServiceFactory downloadServiceFactory,
|
DownloadServiceFactory downloadServiceFactory,
|
||||||
NotificationPublisher notifier
|
INotificationPublisher notifier
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ namespace Infrastructure.Verticals.Jobs;
|
|||||||
|
|
||||||
public class JobChainingListener : IJobListener
|
public class JobChainingListener : IJobListener
|
||||||
{
|
{
|
||||||
|
private readonly string _firstJobName;
|
||||||
private readonly string _nextJobName;
|
private readonly string _nextJobName;
|
||||||
|
|
||||||
public JobChainingListener(string nextJobName)
|
public JobChainingListener(string firstJobName, string nextJobName)
|
||||||
{
|
{
|
||||||
|
_firstJobName = firstJobName;
|
||||||
_nextJobName = nextJobName;
|
_nextJobName = nextJobName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ public class JobChainingListener : IJobListener
|
|||||||
|
|
||||||
public async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken)
|
public async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(_nextJobName) || context.JobDetail.Key.Name == _nextJobName)
|
if (string.IsNullOrEmpty(_nextJobName) || context.JobDetail.Key.Name == _nextJobName || context.JobDetail.Key.Name != _firstJobName)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Domain.Enums;
|
||||||
|
|
||||||
|
namespace Infrastructure.Verticals.Notifications;
|
||||||
|
|
||||||
|
public interface INotificationPublisher
|
||||||
|
{
|
||||||
|
Task NotifyStrike(StrikeType strikeType, int strikeCount);
|
||||||
|
|
||||||
|
Task NotifyQueueItemDeleted(bool removeFromClient, DeleteReason reason);
|
||||||
|
|
||||||
|
Task NotifyDownloadCleaned(double ratio, TimeSpan seedingTime, string categoryName, CleanReason reason);
|
||||||
|
}
|
||||||
@@ -12,25 +12,19 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Infrastructure.Verticals.Notifications;
|
namespace Infrastructure.Verticals.Notifications;
|
||||||
|
|
||||||
public class NotificationPublisher : InterceptedService, IDryRunService
|
public class NotificationPublisher : INotificationPublisher
|
||||||
{
|
{
|
||||||
private readonly ILogger<NotificationPublisher> _logger;
|
private readonly ILogger<INotificationPublisher> _logger;
|
||||||
private readonly IBus _messageBus;
|
private readonly IBus _messageBus;
|
||||||
|
private readonly IDryRunInterceptor _dryRunInterceptor;
|
||||||
/// <summary>
|
|
||||||
/// Constructor to be used by interceptors.
|
public NotificationPublisher(ILogger<INotificationPublisher> logger, IBus messageBus, IDryRunInterceptor dryRunInterceptor)
|
||||||
/// </summary>
|
|
||||||
public NotificationPublisher()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public NotificationPublisher(ILogger<NotificationPublisher> logger, IBus messageBus)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_messageBus = messageBus;
|
_messageBus = messageBus;
|
||||||
|
_dryRunInterceptor = dryRunInterceptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
[DryRunSafeguard]
|
|
||||||
public virtual async Task NotifyStrike(StrikeType strikeType, int strikeCount)
|
public virtual async Task NotifyStrike(StrikeType strikeType, int strikeCount)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -38,7 +32,7 @@ public class NotificationPublisher : InterceptedService, IDryRunService
|
|||||||
QueueRecord record = ContextProvider.Get<QueueRecord>(nameof(QueueRecord));
|
QueueRecord record = ContextProvider.Get<QueueRecord>(nameof(QueueRecord));
|
||||||
InstanceType instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
InstanceType instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||||
Uri instanceUrl = ContextProvider.Get<Uri>(nameof(ArrInstance) + nameof(ArrInstance.Url));
|
Uri instanceUrl = ContextProvider.Get<Uri>(nameof(ArrInstance) + nameof(ArrInstance.Url));
|
||||||
Uri imageUrl = GetImageFromContext(record, instanceType);
|
Uri? imageUrl = GetImageFromContext(record, instanceType);
|
||||||
|
|
||||||
ArrNotification notification = new()
|
ArrNotification notification = new()
|
||||||
{
|
{
|
||||||
@@ -54,10 +48,10 @@ public class NotificationPublisher : InterceptedService, IDryRunService
|
|||||||
switch (strikeType)
|
switch (strikeType)
|
||||||
{
|
{
|
||||||
case StrikeType.Stalled:
|
case StrikeType.Stalled:
|
||||||
await _messageBus.Publish(notification.Adapt<StalledStrikeNotification>());
|
await _dryRunInterceptor.InterceptAsync(Notify<StalledStrikeNotification>, notification.Adapt<StalledStrikeNotification>());
|
||||||
break;
|
break;
|
||||||
case StrikeType.ImportFailed:
|
case StrikeType.ImportFailed:
|
||||||
await _messageBus.Publish(notification.Adapt<FailedImportStrikeNotification>());
|
await _dryRunInterceptor.InterceptAsync(Notify<FailedImportStrikeNotification>, notification.Adapt<FailedImportStrikeNotification>());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,54 +61,84 @@ public class NotificationPublisher : InterceptedService, IDryRunService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[DryRunSafeguard]
|
|
||||||
public virtual async Task NotifyQueueItemDeleted(bool removeFromClient, DeleteReason reason)
|
public virtual async Task NotifyQueueItemDeleted(bool removeFromClient, DeleteReason reason)
|
||||||
{
|
{
|
||||||
QueueRecord record = ContextProvider.Get<QueueRecord>(nameof(QueueRecord));
|
try
|
||||||
InstanceType instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
|
||||||
Uri instanceUrl = ContextProvider.Get<Uri>(nameof(ArrInstance) + nameof(ArrInstance.Url));
|
|
||||||
Uri imageUrl = GetImageFromContext(record, instanceType);
|
|
||||||
|
|
||||||
QueueItemDeletedNotification notification = new()
|
|
||||||
{
|
{
|
||||||
InstanceType = instanceType,
|
QueueRecord record = ContextProvider.Get<QueueRecord>(nameof(QueueRecord));
|
||||||
InstanceUrl = instanceUrl,
|
InstanceType instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||||
Hash = record.DownloadId.ToLowerInvariant(),
|
Uri instanceUrl = ContextProvider.Get<Uri>(nameof(ArrInstance) + nameof(ArrInstance.Url));
|
||||||
Title = $"Deleting item from queue with reason: {reason}",
|
Uri? imageUrl = GetImageFromContext(record, instanceType);
|
||||||
Description = record.Title,
|
|
||||||
Image = imageUrl,
|
QueueItemDeletedNotification notification = new()
|
||||||
Fields = [new() { Title = "Removed from download client?", Text = removeFromClient ? "Yes" : "No" }]
|
{
|
||||||
};
|
InstanceType = instanceType,
|
||||||
|
InstanceUrl = instanceUrl,
|
||||||
await _messageBus.Publish(notification);
|
Hash = record.DownloadId.ToLowerInvariant(),
|
||||||
|
Title = $"Deleting item from queue with reason: {reason}",
|
||||||
|
Description = record.Title,
|
||||||
|
Image = imageUrl,
|
||||||
|
Fields = [new() { Title = "Removed from download client?", Text = removeFromClient ? "Yes" : "No" }]
|
||||||
|
};
|
||||||
|
|
||||||
|
await _dryRunInterceptor.InterceptAsync(Notify<QueueItemDeletedNotification>, notification);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "failed to notify queue item deleted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task NotifyDownloadCleaned(double ratio, TimeSpan seedingTime, string categoryName, CleanReason reason)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DownloadCleanedNotification notification = new()
|
||||||
|
{
|
||||||
|
Title = $"Cleaned item from download client with reason: {reason}",
|
||||||
|
Description = ContextProvider.Get<string>("downloadName"),
|
||||||
|
Fields =
|
||||||
|
[
|
||||||
|
new() { Title = "Hash", Text = ContextProvider.Get<string>("hash").ToLowerInvariant() },
|
||||||
|
new() { Title = "Category", Text = categoryName.ToLowerInvariant() },
|
||||||
|
new() { Title = "Ratio", Text = $"{ratio.ToString(CultureInfo.InvariantCulture)}%" },
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Title = "Seeding hours", Text = $"{Math.Round(seedingTime.TotalHours, 0).ToString(CultureInfo.InvariantCulture)}h"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Level = NotificationLevel.Important
|
||||||
|
};
|
||||||
|
|
||||||
|
await _dryRunInterceptor.InterceptAsync(Notify<DownloadCleanedNotification>, notification);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "failed to notify download cleaned");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[DryRunSafeguard]
|
[DryRunSafeguard]
|
||||||
public virtual async Task NotifyDownloadCleaned(double ratio, TimeSpan seedingTime, string categoryName, CleanReason reason)
|
private Task Notify<T>(T message) where T: notnull
|
||||||
{
|
{
|
||||||
DownloadCleanedNotification notification = new()
|
return _messageBus.Publish(message);
|
||||||
{
|
|
||||||
Title = $"Cleaned item from download client with reason: {reason}",
|
|
||||||
Description = ContextProvider.Get<string>("downloadName"),
|
|
||||||
Fields =
|
|
||||||
[
|
|
||||||
new() { Title = "Hash", Text = ContextProvider.Get<string>("hash").ToLowerInvariant() },
|
|
||||||
new() { Title = "Category", Text = categoryName.ToLowerInvariant() },
|
|
||||||
new() { Title = "Ratio", Text = $"{ratio.ToString(CultureInfo.InvariantCulture)}%" },
|
|
||||||
new() { Title = "Seeding hours", Text = $"{Math.Round(seedingTime.TotalHours, 0).ToString(CultureInfo.InvariantCulture)}h" }
|
|
||||||
],
|
|
||||||
Level = NotificationLevel.Important
|
|
||||||
};
|
|
||||||
|
|
||||||
await _messageBus.Publish(notification);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Uri GetImageFromContext(QueueRecord record, InstanceType instanceType) =>
|
private Uri? GetImageFromContext(QueueRecord record, InstanceType instanceType)
|
||||||
instanceType switch
|
{
|
||||||
|
Uri? image = instanceType switch
|
||||||
{
|
{
|
||||||
InstanceType.Sonarr => record.Series!.Images.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
InstanceType.Sonarr => record.Series?.Images?.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
||||||
InstanceType.Radarr => record.Movie!.Images.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
InstanceType.Radarr => record.Movie?.Images?.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
||||||
InstanceType.Lidarr => record.Album!.Images.FirstOrDefault(x => x.CoverType == "cover")?.Url,
|
InstanceType.Lidarr => record.Album?.Images?.FirstOrDefault(x => x.CoverType == "cover")?.Url,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(instanceType))
|
_ => throw new ArgumentOutOfRangeException(nameof(instanceType))
|
||||||
} ?? throw new Exception("failed to get image url from context");
|
};
|
||||||
|
|
||||||
|
if (image is null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("no poster found for {title}", record.Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ 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;
|
||||||
|
using Infrastructure.Providers;
|
||||||
using Infrastructure.Verticals.Arr;
|
using Infrastructure.Verticals.Arr;
|
||||||
using Infrastructure.Verticals.Arr.Interfaces;
|
using Infrastructure.Verticals.Arr.Interfaces;
|
||||||
using Infrastructure.Verticals.Context;
|
using Infrastructure.Verticals.Context;
|
||||||
@@ -19,7 +20,8 @@ namespace Infrastructure.Verticals.QueueCleaner;
|
|||||||
public sealed class QueueCleaner : GenericHandler
|
public sealed class QueueCleaner : GenericHandler
|
||||||
{
|
{
|
||||||
private readonly QueueCleanerConfig _config;
|
private readonly QueueCleanerConfig _config;
|
||||||
|
private readonly IgnoredDownloadsProvider<QueueCleanerConfig> _ignoredDownloadsProvider;
|
||||||
|
|
||||||
public QueueCleaner(
|
public QueueCleaner(
|
||||||
ILogger<QueueCleaner> logger,
|
ILogger<QueueCleaner> logger,
|
||||||
IOptions<QueueCleanerConfig> config,
|
IOptions<QueueCleanerConfig> config,
|
||||||
@@ -32,7 +34,8 @@ public sealed class QueueCleaner : GenericHandler
|
|||||||
LidarrClient lidarrClient,
|
LidarrClient lidarrClient,
|
||||||
ArrQueueIterator arrArrQueueIterator,
|
ArrQueueIterator arrArrQueueIterator,
|
||||||
DownloadServiceFactory downloadServiceFactory,
|
DownloadServiceFactory downloadServiceFactory,
|
||||||
NotificationPublisher notifier
|
INotificationPublisher notifier,
|
||||||
|
IgnoredDownloadsProvider<QueueCleanerConfig> ignoredDownloadsProvider
|
||||||
) : base(
|
) : base(
|
||||||
logger, downloadClientConfig,
|
logger, downloadClientConfig,
|
||||||
sonarrConfig, radarrConfig, lidarrConfig,
|
sonarrConfig, radarrConfig, lidarrConfig,
|
||||||
@@ -42,10 +45,14 @@ public sealed class QueueCleaner : GenericHandler
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
_config = config.Value;
|
_config = config.Value;
|
||||||
|
_config.Validate();
|
||||||
|
_ignoredDownloadsProvider = ignoredDownloadsProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||||
{
|
{
|
||||||
|
IReadOnlyList<string> ignoredDownloads = await _ignoredDownloadsProvider.GetIgnoredDownloads();
|
||||||
|
|
||||||
using var _ = LogContext.PushProperty("InstanceName", instanceType.ToString());
|
using var _ = LogContext.PushProperty("InstanceName", instanceType.ToString());
|
||||||
|
|
||||||
HashSet<SearchItem> itemsToBeRefreshed = [];
|
HashSet<SearchItem> itemsToBeRefreshed = [];
|
||||||
@@ -75,15 +82,27 @@ public sealed class QueueCleaner : GenericHandler
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ignoredDownloads.Contains(record.DownloadId, StringComparer.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("skip | {title} | ignored", record.Title);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// push record to context
|
// push record to context
|
||||||
ContextProvider.Set(nameof(QueueRecord), record);
|
ContextProvider.Set(nameof(QueueRecord), record);
|
||||||
|
|
||||||
StalledResult stalledCheckResult = new();
|
StalledResult stalledCheckResult = new();
|
||||||
|
|
||||||
if (_downloadClientConfig.DownloadClient is not Common.Enums.DownloadClient.None && record.Protocol is "torrent")
|
if (record.Protocol is "torrent")
|
||||||
{
|
{
|
||||||
|
if (_downloadClientConfig.DownloadClient is Common.Enums.DownloadClient.None)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("skip | download client is not configured | {title}", record.Title);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// stalled download check
|
// stalled download check
|
||||||
stalledCheckResult = await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId);
|
stalledCheckResult = await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId, ignoredDownloads);
|
||||||
}
|
}
|
||||||
|
|
||||||
// failed import check
|
// failed import check
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ignored
|
||||||
@@ -175,6 +175,7 @@ services:
|
|||||||
image: ghcr.io/flmorg/cleanuperr:latest
|
image: ghcr.io/flmorg/cleanuperr:latest
|
||||||
container_name: cleanuperr
|
container_name: cleanuperr
|
||||||
environment:
|
environment:
|
||||||
|
- TZ=Europe/Bucharest
|
||||||
- DRY_RUN=false
|
- DRY_RUN=false
|
||||||
|
|
||||||
- LOGGING__LOGLEVEL=Debug
|
- LOGGING__LOGLEVEL=Debug
|
||||||
@@ -190,6 +191,7 @@ services:
|
|||||||
- TRIGGERS__DOWNLOADCLEANER=0/30 * * * * ?
|
- TRIGGERS__DOWNLOADCLEANER=0/30 * * * * ?
|
||||||
|
|
||||||
- QUEUECLEANER__ENABLED=true
|
- QUEUECLEANER__ENABLED=true
|
||||||
|
- QUEUECLEANER__IGNORED_DOWNLOADS_PATH=/ignored
|
||||||
- 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
|
||||||
@@ -200,10 +202,12 @@ services:
|
|||||||
- QUEUECLEANER__STALLED_DELETE_PRIVATE=false
|
- QUEUECLEANER__STALLED_DELETE_PRIVATE=false
|
||||||
|
|
||||||
- CONTENTBLOCKER__ENABLED=true
|
- CONTENTBLOCKER__ENABLED=true
|
||||||
|
- CONTENTBLOCKER__IGNORED_DOWNLOADS_PATH=/ignored
|
||||||
- CONTENTBLOCKER__IGNORE_PRIVATE=true
|
- CONTENTBLOCKER__IGNORE_PRIVATE=true
|
||||||
- CONTENTBLOCKER__DELETE_PRIVATE=false
|
- CONTENTBLOCKER__DELETE_PRIVATE=false
|
||||||
|
|
||||||
- DOWNLOADCLEANER__ENABLED=true
|
- DOWNLOADCLEANER__ENABLED=true
|
||||||
|
- DOWNLOADCLEANER__IGNORED_DOWNLOADS_PATH=/ignored
|
||||||
- DOWNLOADCLEANER__DELETE_PRIVATE=false
|
- DOWNLOADCLEANER__DELETE_PRIVATE=false
|
||||||
- DOWNLOADCLEANER__CATEGORIES__0__NAME=tv-sonarr
|
- DOWNLOADCLEANER__CATEGORIES__0__NAME=tv-sonarr
|
||||||
- DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO=-1
|
- DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO=-1
|
||||||
@@ -250,10 +254,12 @@ services:
|
|||||||
# - NOTIFIARR__ON_IMPORT_FAILED_STRIKE=true
|
# - NOTIFIARR__ON_IMPORT_FAILED_STRIKE=true
|
||||||
# - NOTIFIARR__ON_STALLED_STRIKE=true
|
# - NOTIFIARR__ON_STALLED_STRIKE=true
|
||||||
# - NOTIFIARR__ON_QUEUE_ITEM_DELETED=true
|
# - NOTIFIARR__ON_QUEUE_ITEM_DELETED=true
|
||||||
|
# - NOTIFIARR__ON_DOWNLOAD_CLEANED=true
|
||||||
# - NOTIFIARR__API_KEY=notifiarr_secret
|
# - NOTIFIARR__API_KEY=notifiarr_secret
|
||||||
# - NOTIFIARR__CHANNEL_ID=discord_channel_id
|
# - NOTIFIARR__CHANNEL_ID=discord_channel_id
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/cleanuperr/logs:/var/logs
|
- ./data/cleanuperr/logs:/var/logs
|
||||||
|
- ./data/cleanuperr/ignored_downloads:/ignored
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- qbittorrent
|
- qbittorrent
|
||||||
|
|||||||
+281
-171
@@ -1,45 +1,52 @@
|
|||||||
## Table of contents
|
## Table of contents
|
||||||
- [General settings](variables.md#general-settings)
|
- [General settings](#general-settings)
|
||||||
- [Queue Cleaner settings](variables.md#queue-cleaner-settings)
|
- [Queue Cleaner settings](#queue-cleaner-settings)
|
||||||
- [Content Blocker settings](variables.md#content-blocker-settings)
|
- [Content Blocker settings](#content-blocker-settings)
|
||||||
- [Download Cleaner settings](variables.md#download-cleaner-settings)
|
- [Download Cleaner settings](#download-cleaner-settings)
|
||||||
- [Download Client settings](variables.md#download-client-settings)
|
- [Download Client settings](#download-client-settings)
|
||||||
- [Arr settings](variables.md#arr-settings)
|
- [Arr settings](#arr-settings)
|
||||||
- [Notification settings](variables.md#notification-settings)
|
- [Notification settings](#notification-settings)
|
||||||
- [Advanced settings](variables.md#advanced-settings)
|
- [Advanced settings](#advanced-settings)
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
||||||
### General settings
|
### General settings
|
||||||
|
|
||||||
**`DRY_RUN`**
|
#### **`TZ`**
|
||||||
|
- The time zone to use.
|
||||||
|
- Type: String.
|
||||||
|
- Possible values: Any valid timezone.
|
||||||
|
- Default: `UTC`.
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`DRY_RUN`**
|
||||||
- When enabled, simulates irreversible operations (like deletions and notifications) without making actual changes.
|
- When enabled, simulates irreversible operations (like deletions and notifications) without making actual changes.
|
||||||
- Type: Boolean.
|
- Type: Boolean.
|
||||||
- Possible values: `true`, `false`.
|
- Possible values: `true`, `false`.
|
||||||
- Default: `false`.
|
- Default: `false`.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`LOGGING__LOGLEVEL`**
|
#### **`LOGGING__LOGLEVEL`**
|
||||||
- Controls the detail level of application logs.
|
- Controls the detail level of application logs.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
- Possible values: `Verbose`, `Debug`, `Information`, `Warning`, `Error`, `Fatal`.
|
- Possible values: `Verbose`, `Debug`, `Information`, `Warning`, `Error`, `Fatal`.
|
||||||
- Default: `Information`.
|
- Default: `Information`.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`LOGGING__FILE__ENABLED`**
|
#### **`LOGGING__FILE__ENABLED`**
|
||||||
- Enables logging to a file.
|
- Enables logging to a file.
|
||||||
- Type: Boolean.
|
- Type: Boolean.
|
||||||
- Possible values: `true`, `false`.
|
- Possible values: `true`, `false`.
|
||||||
- Default: `false`.
|
- Default: `false`.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`LOGGING__FILE__PATH`**
|
#### **`LOGGING__FILE__PATH`**
|
||||||
- Directory where log files will be saved.
|
- Directory where log files will be saved.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
- Default: Empty.
|
- Default: Empty.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`LOGGING__ENHANCED`**
|
#### **`LOGGING__ENHANCED`**
|
||||||
- Provides more detailed descriptions in logs whenever possible.
|
- Provides more detailed descriptions in logs whenever possible.
|
||||||
- Type: Boolean.
|
- Type: Boolean.
|
||||||
- Possible values: `true`, `false`.
|
- Possible values: `true`, `false`.
|
||||||
@@ -51,7 +58,7 @@
|
|||||||
|
|
||||||
### Queue Cleaner settings
|
### Queue Cleaner settings
|
||||||
|
|
||||||
**`TRIGGERS__QUEUECLEANER`**
|
#### **`TRIGGERS__QUEUECLEANER`**
|
||||||
- Cron schedule for the queue cleaner job.
|
- Cron schedule for the queue cleaner job.
|
||||||
- Type: String - [Quartz cron format](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html).
|
- Type: String - [Quartz cron format](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html).
|
||||||
- Default: `0 0/5 * * * ?` (every 5 minutes).
|
- Default: `0 0/5 * * * ?` (every 5 minutes).
|
||||||
@@ -59,9 +66,9 @@
|
|||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> - Maximum interval is 6 hours.
|
> - Maximum interval is 6 hours.
|
||||||
> - Ignored if `QUEUECLEANER__RUNSEQUENTIALLY=true` and `CONTENTBLOCKER__ENABLED=true`.
|
> - Is ignored if `QUEUECLEANER__RUNSEQUENTIALLY=true` and `CONTENTBLOCKER__ENABLED=true`.
|
||||||
|
|
||||||
**`QUEUECLEANER__ENABLED`**
|
#### **`QUEUECLEANER__ENABLED`**
|
||||||
- Enables or disables the queue cleaning functionality.
|
- Enables or disables the queue cleaning functionality.
|
||||||
- When enabled, processes all items in the *arr queue.
|
- When enabled, processes all items in the *arr queue.
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
@@ -69,7 +76,32 @@
|
|||||||
- Default: `true`
|
- Default: `true`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`QUEUECLEANER__RUNSEQUENTIALLY`**
|
#### **`QUEUECLEANER__IGNORED_DOWNLOADS_PATH`**
|
||||||
|
- Local path to the file containing ignored downloads.
|
||||||
|
- If the contents of the file are changed, they will be reloaded on the next job run.
|
||||||
|
- Accepted values:
|
||||||
|
- torrent hash
|
||||||
|
- qBitTorrent tag or category
|
||||||
|
- Deluge label
|
||||||
|
- Transmission category (last directory from the save location)
|
||||||
|
- torrent tracker domain
|
||||||
|
- Each value needs to be on a new line.
|
||||||
|
- Type: String.
|
||||||
|
- Default: Empty.
|
||||||
|
- Required: No.
|
||||||
|
- Example: `/ignored.txt`.
|
||||||
|
- Example of file contents:
|
||||||
|
```
|
||||||
|
fa800a7d7c443a2c3561d1f8f393c089036dade1
|
||||||
|
tv-sonarr
|
||||||
|
qbit-tag
|
||||||
|
mytracker.com
|
||||||
|
...
|
||||||
|
```
|
||||||
|
>[!IMPORTANT]
|
||||||
|
> Some people have experienced problems using Docker where the mounted file would not update inside the container if it was modified on the host. This is a Docker configuration problem and can not be solved by cleanuperr.
|
||||||
|
|
||||||
|
#### **`QUEUECLEANER__RUNSEQUENTIALLY`**
|
||||||
- Controls whether queue cleaner runs after content blocker instead of in parallel.
|
- Controls whether queue cleaner runs after content blocker instead of in parallel.
|
||||||
- When `true`, streamlines the cleaning process by running immediately after content blocker.
|
- When `true`, streamlines the cleaning process by running immediately after content blocker.
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
@@ -77,23 +109,24 @@
|
|||||||
- Default: `true`
|
- Default: `true`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES`**
|
#### **`QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES`**
|
||||||
- Number of strikes before removing a failed import.
|
- Number of strikes before removing a failed import.
|
||||||
- Set to `0` to never remove failed imports.
|
- Set to `0` to never remove failed imports.
|
||||||
- A strike is given when an item is stalled, stuck in metadata downloading, or failed to be imported.
|
- A strike is given when an item fails to be imported.
|
||||||
- Type: Integer
|
- Type: Integer
|
||||||
- Possible values: `0` or greater
|
|
||||||
- Default: `0`
|
- Default: `0`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
> [!NOTE]
|
||||||
|
> If not set to `0`, the minimum value is `3`.
|
||||||
|
|
||||||
**`QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE`**
|
#### **`QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE`**
|
||||||
- Controls whether to ignore failed imports from private trackers.
|
- Controls whether to ignore failed imports from private trackers.
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
- Possible values: `true`, `false`
|
- Possible values: `true`, `false`
|
||||||
- Default: `false`
|
- Default: `false`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE`**
|
#### **`QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE`**
|
||||||
- Controls whether to delete failed imports from private trackers from the download client.
|
- Controls whether to delete failed imports from private trackers from the download client.
|
||||||
- Has no effect if `QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE` is `true`.
|
- Has no effect if `QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE` is `true`.
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
@@ -104,42 +137,43 @@
|
|||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Setting `QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE=true` means you don't care about seeding, ratio, H&R and potentially losing your tracker account.
|
> Setting `QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE=true` means you don't care about seeding, ratio, H&R and potentially losing your tracker account.
|
||||||
|
|
||||||
**`QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS`**
|
#### **`QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS`**
|
||||||
- Patterns to look for in failed import messages that should be ignored.
|
- Patterns to look for in failed import messages that should be ignored.
|
||||||
- Multiple patterns can be specified using incrementing numbers starting from 0.
|
- Multiple patterns can be specified using incrementing numbers starting from 0.
|
||||||
- Type: String array
|
- Type: String array
|
||||||
- Default: Empty.
|
- Default: Empty.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
- Example:
|
- Example:
|
||||||
```yaml
|
```yaml
|
||||||
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`**
|
#### **`QUEUECLEANER__STALLED_MAX_STRIKES`**
|
||||||
- Number of strikes before removing a stalled download.
|
- Number of strikes before removing a stalled download.
|
||||||
- Set to `0` to never remove stalled downloads.
|
- Set to `0` to never remove stalled downloads.
|
||||||
- A strike is given when download speed is 0.
|
- A strike is given when an item is stalled (not downloading) or stuck while downloading metadata.
|
||||||
- Type: Integer
|
- Type: Integer
|
||||||
- Possible values: `0` or greater
|
|
||||||
- Default: `0`
|
- Default: `0`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
> [!NOTE]
|
||||||
|
> If not set to `0`, the minimum value is `3`.
|
||||||
|
|
||||||
**`QUEUECLEANER__STALLED_RESET_STRIKES_ON_PROGRESS`**
|
#### **`QUEUECLEANER__STALLED_RESET_STRIKES_ON_PROGRESS`**
|
||||||
- Controls whether to remove strikes if any download progress was made since last checked.
|
- Controls whether to remove strikes if any download progress was made since last checked.
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
- Possible values: `true`, `false`
|
- Possible values: `true`, `false`
|
||||||
- Default: `false`
|
- Default: `false`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`QUEUECLEANER__STALLED_IGNORE_PRIVATE`**
|
#### **`QUEUECLEANER__STALLED_IGNORE_PRIVATE`**
|
||||||
- Controls whether to ignore stalled downloads from private trackers.
|
- Controls whether to ignore stalled downloads from private trackers.
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
- Possible values: `true`, `false`
|
- Possible values: `true`, `false`
|
||||||
- Default: `false`
|
- Default: `false`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`QUEUECLEANER__STALLED_DELETE_PRIVATE`**
|
#### **`QUEUECLEANER__STALLED_DELETE_PRIVATE`**
|
||||||
- Controls whether to delete stalled private downloads from the download client.
|
- Controls whether to delete stalled private downloads from the download client.
|
||||||
- Has no effect if `QUEUECLEANER__STALLED_IGNORE_PRIVATE` is `true`.
|
- Has no effect if `QUEUECLEANER__STALLED_IGNORE_PRIVATE` is `true`.
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
@@ -154,7 +188,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
|
|
||||||
### Content Blocker settings
|
### Content Blocker settings
|
||||||
|
|
||||||
**`TRIGGERS__CONTENTBLOCKER`**
|
#### **`TRIGGERS__CONTENTBLOCKER`**
|
||||||
- Cron schedule for the content blocker job.
|
- Cron schedule for the content blocker job.
|
||||||
- Type: String - [Quartz cron format](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html).
|
- Type: String - [Quartz cron format](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html).
|
||||||
- Default: `0 0/5 * * * ?` (every 5 minutes).
|
- Default: `0 0/5 * * * ?` (every 5 minutes).
|
||||||
@@ -163,7 +197,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> - Maximum interval is 6 hours.
|
> - Maximum interval is 6 hours.
|
||||||
|
|
||||||
**`CONTENTBLOCKER__ENABLED`**
|
#### **`CONTENTBLOCKER__ENABLED`**
|
||||||
- Enables or disables the content blocker functionality.
|
- Enables or disables the content blocker functionality.
|
||||||
- When enabled, processes all items in the *arr queue and marks unwanted files.
|
- When enabled, processes all items in the *arr queue and marks unwanted files.
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
@@ -171,14 +205,39 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
- Default: `false`
|
- Default: `false`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`CONTENTBLOCKER__IGNORE_PRIVATE`**
|
#### **`CONTENTBLOCKER__IGNORED_DOWNLOADS_PATH`**
|
||||||
|
- Local path to the file containing ignored downloads.
|
||||||
|
- If the contents of the file are changed, they will be reloaded on the next job run.
|
||||||
|
- Accepted values:
|
||||||
|
- torrent hash
|
||||||
|
- qBitTorrent tag or category
|
||||||
|
- Deluge label
|
||||||
|
- Transmission category (last directory from the save location)
|
||||||
|
- torrent tracker domain
|
||||||
|
- Each value needs to be on a new line.
|
||||||
|
- Type: String.
|
||||||
|
- Default: Empty.
|
||||||
|
- Required: No.
|
||||||
|
- Example: `/ignored.txt`.
|
||||||
|
- Example of file contents:
|
||||||
|
```
|
||||||
|
fa800a7d7c443a2c3561d1f8f393c089036dade1
|
||||||
|
tv-sonarr
|
||||||
|
qbit-tag
|
||||||
|
mytracker.com
|
||||||
|
...
|
||||||
|
```
|
||||||
|
>[!IMPORTANT]
|
||||||
|
> Some people have experienced problems using Docker where the mounted file would not update inside the container if it was modified on the host. This is a Docker configuration problem and can not be solved by cleanuperr.
|
||||||
|
|
||||||
|
#### **`CONTENTBLOCKER__IGNORE_PRIVATE`**
|
||||||
- Controls whether to ignore downloads from private trackers.
|
- Controls whether to ignore downloads from private trackers.
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
- Possible values: `true`, `false`
|
- Possible values: `true`, `false`
|
||||||
- Default: `false`
|
- Default: `false`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`CONTENTBLOCKER__DELETE_PRIVATE`**
|
#### **`CONTENTBLOCKER__DELETE_PRIVATE`**
|
||||||
- Controls whether to delete private downloads that have all files blocked from the download client.
|
- Controls whether to delete private downloads that have all files blocked from the download client.
|
||||||
- Has no effect if `CONTENTBLOCKER__IGNORE_PRIVATE` is `true`.
|
- Has no effect if `CONTENTBLOCKER__IGNORE_PRIVATE` is `true`.
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
@@ -193,7 +252,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
|
|
||||||
### Download Cleaner settings
|
### Download Cleaner settings
|
||||||
|
|
||||||
**`TRIGGERS__DOWNLOADCLEANER`**
|
#### **`TRIGGERS__DOWNLOADCLEANER`**
|
||||||
- Cron schedule for the download cleaner job.
|
- Cron schedule for the download cleaner job.
|
||||||
- Type: String - [Quartz cron format](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html).
|
- Type: String - [Quartz cron format](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html).
|
||||||
- Default: `0 0 * * * ?` (every hour).
|
- Default: `0 0 * * * ?` (every hour).
|
||||||
@@ -202,7 +261,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> - Maximum interval is 6 hours.
|
> - Maximum interval is 6 hours.
|
||||||
|
|
||||||
**`DOWNLOADCLEANER__ENABLED`**
|
#### **`DOWNLOADCLEANER__ENABLED`**
|
||||||
- Enables or disables the download cleaner functionality.
|
- Enables or disables the download cleaner functionality.
|
||||||
- When enabled, automatically cleans up downloads that have been seeding for a certain amount of time.
|
- When enabled, automatically cleans up downloads that have been seeding for a certain amount of time.
|
||||||
- Type: Boolean.
|
- Type: Boolean.
|
||||||
@@ -210,7 +269,32 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
- Default: `false`
|
- Default: `false`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`DOWNLOADCLEANER__DELETE_PRIVATE`**
|
#### **`DOWNLOADCLEANER__IGNORED_DOWNLOADS_PATH`**
|
||||||
|
- Local path to the file containing ignored downloads.
|
||||||
|
- If the contents of the file are changed, they will be reloaded on the next job run.
|
||||||
|
- Accepted values:
|
||||||
|
- torrent hash
|
||||||
|
- qBitTorrent tag or category
|
||||||
|
- Deluge label
|
||||||
|
- Transmission category (last directory from the save location)
|
||||||
|
- torrent tracker domain
|
||||||
|
- Each value needs to be on a new line.
|
||||||
|
- Type: String.
|
||||||
|
- Default: Empty.
|
||||||
|
- Required: No.
|
||||||
|
- Example: `/ignored.txt`.
|
||||||
|
- Example of file contents:
|
||||||
|
```
|
||||||
|
fa800a7d7c443a2c3561d1f8f393c089036dade1
|
||||||
|
tv-sonarr
|
||||||
|
qbit-tag
|
||||||
|
mytracker.com
|
||||||
|
...
|
||||||
|
```
|
||||||
|
>[!IMPORTANT]
|
||||||
|
> Some people have experienced problems using Docker where the mounted file would not update inside the container if it was modified on the host. This is a Docker configuration problem and can not be solved by cleanuperr.
|
||||||
|
|
||||||
|
#### **`DOWNLOADCLEANER__DELETE_PRIVATE`**
|
||||||
- Controls whether to delete private downloads.
|
- Controls whether to delete private downloads.
|
||||||
- Type: Boolean.
|
- Type: Boolean.
|
||||||
- Possible values: `true`, `false`
|
- Possible values: `true`, `false`
|
||||||
@@ -220,7 +304,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Setting `DOWNLOADCLEANER__DELETE_PRIVATE=true` means you don't care about seeding, ratio, H&R and potentially losing your tracker account.
|
> Setting `DOWNLOADCLEANER__DELETE_PRIVATE=true` means you don't care about seeding, ratio, H&R and potentially losing your tracker account.
|
||||||
|
|
||||||
**`DOWNLOADCLEANER__CATEGORIES__0__NAME`**
|
#### **`DOWNLOADCLEANER__CATEGORIES__0__NAME`**
|
||||||
- Name of the category to clean.
|
- Name of the category to clean.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
- Default: Empty.
|
- Default: Empty.
|
||||||
@@ -228,18 +312,21 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The category name must match the category that was set in the *arr.
|
> The category name must match the category that was set in the *arr.
|
||||||
|
>
|
||||||
> For qBittorrent, the category name is the name of the download category.
|
> For qBittorrent, the category name is the name of the download category.
|
||||||
|
>
|
||||||
> For Deluge, the category name is the name of the label.
|
> For Deluge, the category name is the name of the label.
|
||||||
> For Transmission, the category name is the name of the download location.
|
>
|
||||||
|
> For Transmission, the category name is the last directory from the save location.
|
||||||
|
|
||||||
**`DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO`**
|
#### **`DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO`**
|
||||||
- Maximum ratio to reach before removing a download.
|
- Maximum ratio to reach before removing a download.
|
||||||
- Type: Decimal.
|
- Type: Decimal.
|
||||||
- Possible values: `-1` or greater (`-1` means no limit or disabled).
|
- Possible values: `-1` or greater (`-1` means no limit or disabled).
|
||||||
- Default: `-1`
|
- Default: `-1`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`DOWNLOADCLEANER__CATEGORIES__0__MIN_SEED_TIME`**
|
#### **`DOWNLOADCLEANER__CATEGORIES__0__MIN_SEED_TIME`**
|
||||||
- Minimum number of hours to seed before removing a download, if the ratio has been met.
|
- Minimum number of hours to seed before removing a download, if the ratio has been met.
|
||||||
- Used with `MAX_RATIO` to ensure a minimum seed time.
|
- Used with `MAX_RATIO` to ensure a minimum seed time.
|
||||||
- Type: Decimal.
|
- Type: Decimal.
|
||||||
@@ -247,7 +334,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
- Default: `0`
|
- Default: `0`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`DOWNLOADCLEANER__CATEGORIES__0__MAX_SEED_TIME`**
|
#### **`DOWNLOADCLEANER__CATEGORIES__0__MAX_SEED_TIME`**
|
||||||
- Maximum number of hours to seed before removing a download.
|
- Maximum number of hours to seed before removing a download.
|
||||||
- Type: Decimal.
|
- Type: Decimal.
|
||||||
- Possible values: `-1` or greater (`-1` means no limit or disabled).
|
- Possible values: `-1` or greater (`-1` means no limit or disabled).
|
||||||
@@ -270,7 +357,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
|
|
||||||
### Download Client settings
|
### Download Client settings
|
||||||
|
|
||||||
**`DOWNLOAD_CLIENT`**
|
#### **`DOWNLOAD_CLIENT`**
|
||||||
- Specifies which download client is used by *arrs.
|
- Specifies which download client is used by *arrs.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
- Possible values: `none`, `qbittorrent`, `deluge`, `transmission`.
|
- Possible values: `none`, `qbittorrent`, `deluge`, `transmission`.
|
||||||
@@ -280,49 +367,49 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> 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.
|
> 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.
|
||||||
|
|
||||||
**`QBITTORRENT__URL`**
|
#### **`QBITTORRENT__URL`**
|
||||||
- URL of the qBittorrent instance.
|
- URL of the qBittorrent instance.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
- Default: `http://localhost:8080`.
|
- Default: `http://localhost:8080`.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`QBITTORRENT__USERNAME`**
|
#### **`QBITTORRENT__USERNAME`**
|
||||||
- Username for qBittorrent authentication.
|
- Username for qBittorrent authentication.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
- Default: Empty.
|
- Default: Empty.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`QBITTORRENT__PASSWORD`**
|
#### **`QBITTORRENT__PASSWORD`**
|
||||||
- Password for qBittorrent authentication.
|
- Password for qBittorrent authentication.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
- Default: Empty.
|
- Default: Empty.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`DELUGE__URL`**
|
#### **`DELUGE__URL`**
|
||||||
- URL of the Deluge instance.
|
- URL of the Deluge instance.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
- Default: `http://localhost:8112`.
|
- Default: `http://localhost:8112`.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`DELUGE__PASSWORD`**
|
#### **`DELUGE__PASSWORD`**
|
||||||
- Password for Deluge authentication.
|
- Password for Deluge authentication.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
- Default: Empty.
|
- Default: Empty.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`TRANSMISSION__URL`**
|
#### **`TRANSMISSION__URL`**
|
||||||
- URL of the Transmission instance.
|
- URL of the Transmission instance.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
- Default: `http://localhost:9091`.
|
- Default: `http://localhost:9091`.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`TRANSMISSION__USERNAME`**
|
#### **`TRANSMISSION__USERNAME`**
|
||||||
- Username for Transmission authentication.
|
- Username for Transmission authentication.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
- Default: Empty.
|
- Default: Empty.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`TRANSMISSION__PASSWORD`**
|
#### **`TRANSMISSION__PASSWORD`**
|
||||||
- Password for Transmission authentication.
|
- Password for Transmission authentication.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
- Default: Empty.
|
- Default: Empty.
|
||||||
@@ -332,112 +419,6 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
|
|
||||||
### Arr settings
|
### Arr settings
|
||||||
|
|
||||||
**`SONARR__ENABLED`**
|
|
||||||
- Enables or disables Sonarr cleanup.
|
|
||||||
- Type: Boolean
|
|
||||||
- Possible values: `true`, `false`
|
|
||||||
- Default: `false`
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`SONARR__BLOCK__TYPE`**
|
|
||||||
- Determines how file blocking works for Sonarr.
|
|
||||||
- Type: String
|
|
||||||
- Possible values: `blacklist`, `whitelist`
|
|
||||||
- Default: `blacklist`
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`SONARR__BLOCK__PATH`**
|
|
||||||
- Path to the blocklist file (local file or URL).
|
|
||||||
- Must be JSON compatible.
|
|
||||||
- Type: String
|
|
||||||
- Default: Empty.
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`SONARR__SEARCHTYPE`**
|
|
||||||
- Determines what to search for after removing a queue item.
|
|
||||||
- Type: String
|
|
||||||
- Possible values: `Episode`, `Season`, `Series`
|
|
||||||
- Default: `Episode`
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`SONARR__INSTANCES__0__URL`**
|
|
||||||
- URL of the Sonarr instance.
|
|
||||||
- Type: String
|
|
||||||
- Default: `http://localhost:8989`
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`SONARR__INSTANCES__0__APIKEY`**
|
|
||||||
- API key for the Sonarr instance.
|
|
||||||
- Type: String
|
|
||||||
- Default: Empty.
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`RADARR__ENABLED`**
|
|
||||||
- Enables or disables Radarr cleanup.
|
|
||||||
- Type: Boolean
|
|
||||||
- Possible values: `true`, `false`
|
|
||||||
- Default: `false`
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`RADARR__BLOCK__TYPE`**
|
|
||||||
- Determines how file blocking works for Radarr.
|
|
||||||
- Type: String
|
|
||||||
- Possible values: `blacklist`, `whitelist`
|
|
||||||
- Default: `blacklist`
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`RADARR__BLOCK__PATH`**
|
|
||||||
- Path to the blocklist file (local file or URL).
|
|
||||||
- Must be JSON compatible.
|
|
||||||
- Type: String
|
|
||||||
- Default: Empty.
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`RADARR__INSTANCES__0__URL`**
|
|
||||||
- URL of the Radarr instance.
|
|
||||||
- Type: String
|
|
||||||
- Default: `http://localhost:7878`
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`RADARR__INSTANCES__0__APIKEY`**
|
|
||||||
- API key for the Radarr instance.
|
|
||||||
- Type: String
|
|
||||||
- Default: Empty.
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`LIDARR__ENABLED`**
|
|
||||||
- Enables or disables Lidarr cleanup.
|
|
||||||
- Type: Boolean
|
|
||||||
- Possible values: `true`, `false`
|
|
||||||
- Default: `false`
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`LIDARR__BLOCK__TYPE`**
|
|
||||||
- Determines how file blocking works for Lidarr.
|
|
||||||
- Type: String
|
|
||||||
- Possible values: `blacklist`, `whitelist`
|
|
||||||
- Default: `blacklist`
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`LIDARR__BLOCK__PATH`**
|
|
||||||
- Path to the blocklist file (local file or URL).
|
|
||||||
- Must be JSON compatible.
|
|
||||||
- Type: String
|
|
||||||
- Default: Empty.
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`LIDARR__INSTANCES__0__URL`**
|
|
||||||
- URL of the Lidarr instance.
|
|
||||||
- Type: String
|
|
||||||
- Default: `http://localhost:8686`
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
**`LIDARR__INSTANCES__0__APIKEY`**
|
|
||||||
- API key for the Lidarr instance.
|
|
||||||
- Type: String
|
|
||||||
- Default: Empty.
|
|
||||||
- Required: No.
|
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Multiple instances can be specified for each *arr using this format, where `<NUMBER>` starts from 0:
|
> Multiple instances can be specified for each *arr using this format, where `<NUMBER>` starts from 0:
|
||||||
> ```yaml
|
> ```yaml
|
||||||
@@ -445,8 +426,29 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
> <ARR>__INSTANCES__<NUMBER>__APIKEY
|
> <ARR>__INSTANCES__<NUMBER>__APIKEY
|
||||||
> ```
|
> ```
|
||||||
|
|
||||||
|
#### **`SONARR__ENABLED`**
|
||||||
|
- Enables or disables Sonarr cleanup.
|
||||||
|
- Type: Boolean
|
||||||
|
- Possible values: `true`, `false`
|
||||||
|
- Default: `false`
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`SONARR__BLOCK__TYPE`**
|
||||||
|
- Determines how file blocking works for Sonarr.
|
||||||
|
- Type: String
|
||||||
|
- Possible values: `blacklist`, `whitelist`
|
||||||
|
- Default: `blacklist`
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`SONARR__BLOCK__PATH`**
|
||||||
|
- Path to the blocklist file (local file or URL).
|
||||||
|
- Must be JSON compatible.
|
||||||
|
- Type: String
|
||||||
|
- Default: Empty.
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The blocklists (blacklist/whitelist) support the following patterns:
|
> The blocklists support the following patterns:
|
||||||
> ```
|
> ```
|
||||||
> *example // file name ends with "example"
|
> *example // file name ends with "example"
|
||||||
> example* // file name starts with "example"
|
> example* // file name starts with "example"
|
||||||
@@ -456,47 +458,155 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
> ```
|
> ```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> [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.
|
> [This blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist), [this permissive blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist_permissive) and [this whitelist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/whitelist) can be used for Sonarr and Radarr.
|
||||||
|
|
||||||
|
#### **`SONARR__SEARCHTYPE`**
|
||||||
|
- Determines what to search for after removing a queue item.
|
||||||
|
- Type: String
|
||||||
|
- Possible values: `Episode`, `Season`, `Series`
|
||||||
|
- Default: `Episode`
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`SONARR__INSTANCES__0__URL`**
|
||||||
|
- URL of the Sonarr instance.
|
||||||
|
- Type: String
|
||||||
|
- Default: `http://localhost:8989`
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`SONARR__INSTANCES__0__APIKEY`**
|
||||||
|
- API key for the Sonarr instance.
|
||||||
|
- Type: String
|
||||||
|
- Default: Empty.
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`RADARR__ENABLED`**
|
||||||
|
- Enables or disables Radarr cleanup.
|
||||||
|
- Type: Boolean
|
||||||
|
- Possible values: `true`, `false`
|
||||||
|
- Default: `false`
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`RADARR__BLOCK__TYPE`**
|
||||||
|
- Determines how file blocking works for Radarr.
|
||||||
|
- Type: String
|
||||||
|
- Possible values: `blacklist`, `whitelist`
|
||||||
|
- Default: `blacklist`
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`RADARR__BLOCK__PATH`**
|
||||||
|
- Path to the blocklist file (local file or URL).
|
||||||
|
- Must be JSON compatible.
|
||||||
|
- Type: String
|
||||||
|
- Default: Empty.
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The blocklists support the following patterns:
|
||||||
|
> ```
|
||||||
|
> *example // file name ends with "example"
|
||||||
|
> example* // file name starts with "example"
|
||||||
|
> *example* // file name has "example" in the name
|
||||||
|
> 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:"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> [This blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist), [this permissive blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist_permissive) and [this whitelist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/whitelist) can be used for Sonarr and Radarr.
|
||||||
|
|
||||||
|
#### **`RADARR__INSTANCES__0__URL`**
|
||||||
|
- URL of the Radarr instance.
|
||||||
|
- Type: String
|
||||||
|
- Default: `http://localhost:7878`
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`RADARR__INSTANCES__0__APIKEY`**
|
||||||
|
- API key for the Radarr instance.
|
||||||
|
- Type: String
|
||||||
|
- Default: Empty.
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`LIDARR__ENABLED`**
|
||||||
|
- Enables or disables Lidarr cleanup.
|
||||||
|
- Type: Boolean
|
||||||
|
- Possible values: `true`, `false`
|
||||||
|
- Default: `false`
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`LIDARR__BLOCK__TYPE`**
|
||||||
|
- Determines how file blocking works for Lidarr.
|
||||||
|
- Type: String
|
||||||
|
- Possible values: `blacklist`, `whitelist`
|
||||||
|
- Default: `blacklist`
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`LIDARR__BLOCK__PATH`**
|
||||||
|
- Path to the blocklist file (local file or URL).
|
||||||
|
- Must be JSON compatible.
|
||||||
|
- Type: String
|
||||||
|
- Default: Empty.
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The blocklists support the following patterns:
|
||||||
|
> ```
|
||||||
|
> *example // file name ends with "example"
|
||||||
|
> example* // file name starts with "example"
|
||||||
|
> *example* // file name has "example" in the name
|
||||||
|
> 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:"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
#### **`LIDARR__INSTANCES__0__URL`**
|
||||||
|
- URL of the Lidarr instance.
|
||||||
|
- Type: String
|
||||||
|
- Default: `http://localhost:8686`
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`LIDARR__INSTANCES__0__APIKEY`**
|
||||||
|
- API key for the Lidarr instance.
|
||||||
|
- Type: String
|
||||||
|
- Default: Empty.
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
||||||
### Notification settings
|
### Notification settings
|
||||||
|
|
||||||
**`NOTIFIARR__API_KEY`**
|
#### **`NOTIFIARR__API_KEY`**
|
||||||
- Notifiarr API key for sending notifications.
|
- Notifiarr API key for sending notifications.
|
||||||
- Requires Notifiarr's [`Passthrough`](https://notifiarr.wiki/en/Website/Integrations/Passthrough) integration to work.
|
- Requires Notifiarr's [`Passthrough`](https://notifiarr.wiki/en/Website/Integrations/Passthrough) integration to work.
|
||||||
- Type: String
|
- Type: String
|
||||||
- Default: Empty.
|
- Default: Empty.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`NOTIFIARR__CHANNEL_ID`**
|
#### **`NOTIFIARR__CHANNEL_ID`**
|
||||||
- Discord channel ID where notifications will be sent.
|
- Discord channel ID where notifications will be sent.
|
||||||
- Type: String
|
- Type: String
|
||||||
- Default: Empty.
|
- Default: Empty.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`NOTIFIARR__ON_IMPORT_FAILED_STRIKE`**
|
#### **`NOTIFIARR__ON_IMPORT_FAILED_STRIKE`**
|
||||||
- Controls whether to notify when an item receives a failed import strike.
|
- Controls whether to notify when an item receives a failed import strike.
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
- Possible values: `true`, `false`
|
- Possible values: `true`, `false`
|
||||||
- Default: `false`
|
- Default: `false`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`NOTIFIARR__ON_STALLED_STRIKE`**
|
#### **`NOTIFIARR__ON_STALLED_STRIKE`**
|
||||||
- Controls whether to notify when an item receives a stalled download strike.
|
- Controls whether to notify when an item receives a stalled download strike.
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
- Possible values: `true`, `false`
|
- Possible values: `true`, `false`
|
||||||
- Default: `false`
|
- Default: `false`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`NOTIFIARR__ON_QUEUE_ITEM_DELETED`**
|
#### **`NOTIFIARR__ON_QUEUE_ITEM_DELETED`**
|
||||||
- Controls whether to notify when a queue item is deleted.
|
- Controls whether to notify when a queue item is deleted.
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
- Possible values: `true`, `false`
|
- Possible values: `true`, `false`
|
||||||
- Default: `false`
|
- Default: `false`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`NOTIFIARR__ON_DOWNLOAD_CLEANED`**
|
#### **`NOTIFIARR__ON_DOWNLOAD_CLEANED`**
|
||||||
- Controls whether to notify when a download is cleaned.
|
- Controls whether to notify when a download is cleaned.
|
||||||
- Type: Boolean
|
- Type: Boolean
|
||||||
- Possible values: `true`, `false`
|
- Possible values: `true`, `false`
|
||||||
@@ -507,7 +617,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
|
|
||||||
### Advanced settings
|
### Advanced settings
|
||||||
|
|
||||||
**`HTTP_MAX_RETRIES`**
|
#### **`HTTP_MAX_RETRIES`**
|
||||||
- The number of times to retry a failed HTTP call.
|
- The number of times to retry a failed HTTP call.
|
||||||
- Applies to calls to *arrs, download clients, and other services.
|
- Applies to calls to *arrs, download clients, and other services.
|
||||||
- Type: Integer
|
- Type: Integer
|
||||||
@@ -515,10 +625,10 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
|||||||
- Default: `0`
|
- Default: `0`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
**`HTTP_TIMEOUT`**
|
#### **`HTTP_TIMEOUT`**
|
||||||
- The number of seconds to wait before failing an HTTP call.
|
- The number of seconds to wait before failing an HTTP call.
|
||||||
- Applies to calls to *arrs, download clients, and other services.
|
- Applies to calls to *arrs, download clients, and other services.
|
||||||
- Type: Integer
|
- Type: Integer
|
||||||
- Possible values: Greater than `0`
|
- Possible values: Greater than `0`.
|
||||||
- Default: `100`
|
- Default: `100`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
Reference in New Issue
Block a user