Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75492a5792 | |||
| 5ca717d7e0 | |||
| 7068ee5e5a | |||
| 9f770473e5 | |||
| 5fe0f5750a | |||
| b8ce225ccc | |||
| f21f7388b7 | |||
| a1354f231a | |||
| 4bc1c33e81 | |||
| 32bcbab523 | |||
| b94ae21e11 |
@@ -40,6 +40,7 @@ cleanuperr supports both qBittorrent's built-in exclusion features and its own b
|
|||||||
- [Windows](#windows)
|
- [Windows](#windows)
|
||||||
- [Linux](#linux)
|
- [Linux](#linux)
|
||||||
- [MacOS](#macos)
|
- [MacOS](#macos)
|
||||||
|
- [FreeBSD](#freebsd)
|
||||||
- [Credits](#credits)
|
- [Credits](#credits)
|
||||||
|
|
||||||
## Naming choice
|
## Naming choice
|
||||||
@@ -94,7 +95,7 @@ I've seen a few discussions on this type of naming and I've decided that I didn'
|
|||||||
- If it is, the item receives a **strike** and will continue to accumulate strikes every time it meets any of these conditions.
|
- If it is, the item receives a **strike** and will continue to accumulate strikes every time it meets any of these conditions.
|
||||||
- Check each queue item if it meets one of the following condition in the download client:
|
- Check each queue item if it meets one of the following condition in the download client:
|
||||||
- **Marked as completed, but 0 bytes have been downloaded** (due to files being blocked by qBittorrent or the **content blocker**).
|
- **Marked as completed, but 0 bytes have been downloaded** (due to files being blocked by qBittorrent or the **content blocker**).
|
||||||
- All associated files of are marked as **unwanted/skipped**.
|
- All associated files are marked as **unwanted/skipped/do not download**.
|
||||||
- If the item **DOES NOT** match the above criteria, it will be skipped.
|
- If the item **DOES NOT** match the above criteria, it will be skipped.
|
||||||
- If the item **DOES** match the criteria or has received the **maximum number of strikes**:
|
- If the item **DOES** match the criteria or has received the **maximum number of strikes**:
|
||||||
- It will be removed from the *arr's queue and blocked.
|
- It will be removed from the *arr's queue and blocked.
|
||||||
@@ -213,15 +214,18 @@ services:
|
|||||||
# OR
|
# OR
|
||||||
# - DOWNLOAD_CLIENT=qBittorrent
|
# - DOWNLOAD_CLIENT=qBittorrent
|
||||||
# - QBITTORRENT__URL=http://localhost:8080
|
# - QBITTORRENT__URL=http://localhost:8080
|
||||||
|
# - QBITTORRENT__URL_BASE=myCustomPath
|
||||||
# - QBITTORRENT__USERNAME=user
|
# - QBITTORRENT__USERNAME=user
|
||||||
# - QBITTORRENT__PASSWORD=pass
|
# - QBITTORRENT__PASSWORD=pass
|
||||||
# OR
|
# OR
|
||||||
# - DOWNLOAD_CLIENT=deluge
|
# - DOWNLOAD_CLIENT=deluge
|
||||||
|
# - DELUGE__URL_BASE=myCustomPath
|
||||||
# - DELUGE__URL=http://localhost:8112
|
# - DELUGE__URL=http://localhost:8112
|
||||||
# - DELUGE__PASSWORD=testing
|
# - DELUGE__PASSWORD=testing
|
||||||
# OR
|
# OR
|
||||||
# - DOWNLOAD_CLIENT=transmission
|
# - DOWNLOAD_CLIENT=transmission
|
||||||
# - TRANSMISSION__URL=http://localhost:9091
|
# - TRANSMISSION__URL=http://localhost:9091
|
||||||
|
# - TRANSMISSION__URL_BASE=myCustomPath
|
||||||
# - TRANSMISSION__USERNAME=test
|
# - TRANSMISSION__USERNAME=test
|
||||||
# - TRANSMISSION__PASSWORD=testing
|
# - TRANSMISSION__PASSWORD=testing
|
||||||
|
|
||||||
@@ -267,7 +271,21 @@ services:
|
|||||||
|
|
||||||
> [!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
|
> 1. Download latest nssm build from `https://nssm.cc/builds`.
|
||||||
|
> 2. Unzip `nssm.exe` in `C:\example\directory`.
|
||||||
|
> 3. Open a terminal with Administrator rights and execute these commands:
|
||||||
|
> ```
|
||||||
|
> nssm.exe install Cleanuperr "C:\example\directory\cleanuperr.exe"
|
||||||
|
> nssm.exe set Cleanuperr AppDirectory "C:\example\directory\"
|
||||||
|
> nssm.exe set Cleanuperr AppStdout "C:\example\directory\cleanuperr.log"
|
||||||
|
> nssm.exe set Cleanuperr AppStderr "C:\example\directory\cleanuperr.crash.log"
|
||||||
|
> nssm.exe set Cleanuperr AppRotateFiles 1
|
||||||
|
> nssm.exe set Cleanuperr AppRotateOnline 1
|
||||||
|
> nssm.exe set Cleanuperr AppRotateBytes 10485760
|
||||||
|
> nssm.exe set Cleanuperr AppRotateFiles 10
|
||||||
|
> nssm.exe set Cleanuperr Start SERVICE_AUTO_START
|
||||||
|
> nssm.exe start Cleanuperr
|
||||||
|
> ```
|
||||||
|
|
||||||
### <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>
|
### <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>
|
||||||
|
|
||||||
@@ -300,6 +318,54 @@ services:
|
|||||||
> codesign --sign - --force --preserve-metadata=entitlements,requirements,flags,runtime /example/directory/cleanuperr
|
> codesign --sign - --force --preserve-metadata=entitlements,requirements,flags,runtime /example/directory/cleanuperr
|
||||||
> ```
|
> ```
|
||||||
|
|
||||||
|
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/freebsd.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">FreeBSD</span>
|
||||||
|
|
||||||
|
1. Installation:
|
||||||
|
```
|
||||||
|
# install dependencies
|
||||||
|
pkg install -y git icu libinotify libunwind wget
|
||||||
|
|
||||||
|
# set up the dotnet SDK
|
||||||
|
cd ~
|
||||||
|
wget -q https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/v9.0.104-amd64-freebsd-14/dotnet-sdk-9.0.104-freebsd-x64.tar.gz
|
||||||
|
export DOTNET_ROOT=$(pwd)/.dotnet
|
||||||
|
mkdir -p "$DOTNET_ROOT" && tar zxf dotnet-sdk-9.0.104-freebsd-x64.tar.gz -C "$DOTNET_ROOT"
|
||||||
|
export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools
|
||||||
|
|
||||||
|
# download NuGet dependencies
|
||||||
|
mkdir -p /tmp/nuget
|
||||||
|
wget -q -P /tmp/nuget/ https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/v9.0.104-amd64-freebsd-14/Microsoft.AspNetCore.App.Runtime.freebsd-x64.9.0.3.nupkg
|
||||||
|
wget -q -P /tmp/nuget/ https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/v9.0.104-amd64-freebsd-14/Microsoft.NETCore.App.Host.freebsd-x64.9.0.3.nupkg
|
||||||
|
wget -q -P /tmp/nuget/ https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/v9.0.104-amd64-freebsd-14/Microsoft.NETCore.App.Runtime.freebsd-x64.9.0.3.nupkg
|
||||||
|
|
||||||
|
# add NuGet source
|
||||||
|
dotnet nuget add source /tmp/nuget --name tmp
|
||||||
|
|
||||||
|
# add GitHub NuGet source
|
||||||
|
# a PAT (Personal Access Token) can be generated here https://github.com/settings/tokens
|
||||||
|
dotnet nuget add source --username <YOUR_USERNAME> --password <YOUR_PERSONAL_ACCESS_TOKEN> --store-password-in-clear-text --name flmorg https://nuget.pkg.github.com/flmorg/index.json
|
||||||
|
```
|
||||||
|
2. Building:
|
||||||
|
```
|
||||||
|
# clone the project
|
||||||
|
git clone https://github.com/flmorg/cleanuperr.git
|
||||||
|
cd cleanuperr
|
||||||
|
|
||||||
|
# build and publish the app
|
||||||
|
dotnet publish code/Executable/Executable.csproj -c Release --self-contained -o artifacts /p:PublishSingleFile=true
|
||||||
|
|
||||||
|
# move the files to permanent destination
|
||||||
|
mv artifacts/cleanuperr /example/directory/
|
||||||
|
mv artifacts/appsettings.json /example/directory/
|
||||||
|
```
|
||||||
|
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [here](#environment-variables).
|
||||||
|
4. Run the app:
|
||||||
|
```
|
||||||
|
cd /example/directory
|
||||||
|
chmod +x cleanuperr
|
||||||
|
./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)
|
||||||
|
|||||||
@@ -216,8 +216,6 @@
|
|||||||
*.log
|
*.log
|
||||||
*.loop-vbs
|
*.loop-vbs
|
||||||
*.ls
|
*.ls
|
||||||
*.m3u
|
|
||||||
*.m4a
|
|
||||||
*.mac
|
*.mac
|
||||||
*.macho
|
*.macho
|
||||||
*.mamc
|
*.mamc
|
||||||
@@ -271,7 +269,6 @@
|
|||||||
*.ncl
|
*.ncl
|
||||||
*.net
|
*.net
|
||||||
*.nexe
|
*.nexe
|
||||||
*.nfo
|
|
||||||
*.nrg
|
*.nrg
|
||||||
*.num
|
*.num
|
||||||
*.nzb.bz2
|
*.nzb.bz2
|
||||||
@@ -402,7 +399,6 @@
|
|||||||
*.sql
|
*.sql
|
||||||
*.sqx
|
*.sqx
|
||||||
*.srec
|
*.srec
|
||||||
*.srt
|
|
||||||
*.ssm
|
*.ssm
|
||||||
*.sts
|
*.sts
|
||||||
*.sub
|
*.sub
|
||||||
@@ -514,6 +510,4 @@
|
|||||||
*sample.mp4
|
*sample.mp4
|
||||||
*sample.webm
|
*sample.webm
|
||||||
*sample.wmv
|
*sample.wmv
|
||||||
Trailer.*
|
|
||||||
VOSTFR
|
|
||||||
api
|
api
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
*.apk
|
*.apk
|
||||||
|
*.arj
|
||||||
*.bat
|
*.bat
|
||||||
*.bin
|
*.bin
|
||||||
*.bmp
|
*.bmp
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Common.Exceptions;
|
using Common.Exceptions;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Common.Configuration.DownloadClient;
|
namespace Common.Configuration.DownloadClient;
|
||||||
|
|
||||||
@@ -8,6 +9,9 @@ public sealed record DelugeConfig : IConfig
|
|||||||
|
|
||||||
public Uri? Url { get; init; }
|
public Uri? Url { get; init; }
|
||||||
|
|
||||||
|
[ConfigurationKeyName("URL_BASE")]
|
||||||
|
public string UrlBase { get; init; } = string.Empty;
|
||||||
|
|
||||||
public string? Password { get; init; }
|
public string? Password { get; init; }
|
||||||
|
|
||||||
public void Validate()
|
public void Validate()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Common.Exceptions;
|
using Common.Exceptions;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Common.Configuration.DownloadClient;
|
namespace Common.Configuration.DownloadClient;
|
||||||
|
|
||||||
@@ -8,6 +9,9 @@ public sealed class QBitConfig : IConfig
|
|||||||
|
|
||||||
public Uri? Url { get; init; }
|
public Uri? Url { get; init; }
|
||||||
|
|
||||||
|
[ConfigurationKeyName("URL_BASE")]
|
||||||
|
public string UrlBase { get; init; } = string.Empty;
|
||||||
|
|
||||||
public string? Username { get; init; }
|
public string? Username { get; init; }
|
||||||
|
|
||||||
public string? Password { get; init; }
|
public string? Password { get; init; }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Common.Exceptions;
|
using Common.Exceptions;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Common.Configuration.DownloadClient;
|
namespace Common.Configuration.DownloadClient;
|
||||||
|
|
||||||
@@ -8,6 +9,9 @@ public record TransmissionConfig : IConfig
|
|||||||
|
|
||||||
public Uri? Url { get; init; }
|
public Uri? Url { get; init; }
|
||||||
|
|
||||||
|
[ConfigurationKeyName("URL_BASE")]
|
||||||
|
public string UrlBase { get; init; } = "transmission";
|
||||||
|
|
||||||
public string? Username { get; init; }
|
public string? Username { get; init; }
|
||||||
|
|
||||||
public string? Password { get; init; }
|
public string? Password { get; init; }
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ public enum DownloadClient
|
|||||||
QBittorrent,
|
QBittorrent,
|
||||||
Deluge,
|
Deluge,
|
||||||
Transmission,
|
Transmission,
|
||||||
None
|
None,
|
||||||
|
Disabled
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
public enum DeleteReason
|
public enum DeleteReason
|
||||||
{
|
{
|
||||||
|
None,
|
||||||
Stalled,
|
Stalled,
|
||||||
ImportFailed,
|
ImportFailed,
|
||||||
AllFilesBlocked
|
DownloadingMetadata,
|
||||||
|
AllFilesSkipped,
|
||||||
|
AllFilesSkippedByQBit,
|
||||||
|
AllFilesBlocked,
|
||||||
}
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
public enum StrikeType
|
public enum StrikeType
|
||||||
{
|
{
|
||||||
Stalled,
|
Stalled,
|
||||||
|
DownloadingMetadata,
|
||||||
ImportFailed
|
ImportFailed
|
||||||
}
|
}
|
||||||
@@ -52,15 +52,18 @@
|
|||||||
"DOWNLOAD_CLIENT": "qbittorrent",
|
"DOWNLOAD_CLIENT": "qbittorrent",
|
||||||
"qBittorrent": {
|
"qBittorrent": {
|
||||||
"Url": "http://localhost:8080",
|
"Url": "http://localhost:8080",
|
||||||
|
"URL_BASE": "",
|
||||||
"Username": "test",
|
"Username": "test",
|
||||||
"Password": "testing"
|
"Password": "testing"
|
||||||
},
|
},
|
||||||
"Deluge": {
|
"Deluge": {
|
||||||
"Url": "http://localhost:8112",
|
"Url": "http://localhost:8112",
|
||||||
|
"URL_BASE": "",
|
||||||
"Password": "testing"
|
"Password": "testing"
|
||||||
},
|
},
|
||||||
"Transmission": {
|
"Transmission": {
|
||||||
"Url": "http://localhost:9091",
|
"Url": "http://localhost:9091",
|
||||||
|
"URL_BASE": "transmission",
|
||||||
"Username": "test",
|
"Username": "test",
|
||||||
"Password": "testing"
|
"Password": "testing"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,15 +42,18 @@
|
|||||||
"DOWNLOAD_CLIENT": "none",
|
"DOWNLOAD_CLIENT": "none",
|
||||||
"qBittorrent": {
|
"qBittorrent": {
|
||||||
"Url": "http://localhost:8080",
|
"Url": "http://localhost:8080",
|
||||||
|
"URL_BASE": "",
|
||||||
"Username": "",
|
"Username": "",
|
||||||
"Password": ""
|
"Password": ""
|
||||||
},
|
},
|
||||||
"Deluge": {
|
"Deluge": {
|
||||||
"Url": "http://localhost:8112",
|
"Url": "http://localhost:8112",
|
||||||
|
"URL_BASE": "",
|
||||||
"Password": "testing"
|
"Password": "testing"
|
||||||
},
|
},
|
||||||
"Transmission": {
|
"Transmission": {
|
||||||
"Url": "http://localhost:9091",
|
"Url": "http://localhost:9091",
|
||||||
|
"URL_BASE": "transmission",
|
||||||
"Username": "test",
|
"Username": "test",
|
||||||
"Password": "testing"
|
"Password": "testing"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -105,13 +105,14 @@ public class DownloadServiceTests : IClassFixture<DownloadServiceFixture>
|
|||||||
// Arrange
|
// Arrange
|
||||||
const string hash = "test-hash";
|
const string hash = "test-hash";
|
||||||
const string itemName = "test-item";
|
const string itemName = "test-item";
|
||||||
_fixture.Striker.StrikeAndCheckLimit(hash, itemName, 3, StrikeType.Stalled)
|
StrikeType strikeType = StrikeType.Stalled;
|
||||||
|
_fixture.Striker.StrikeAndCheckLimit(hash, itemName, 3, strikeType)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
TestDownloadService sut = _fixture.CreateSut();
|
TestDownloadService sut = _fixture.CreateSut();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
bool result = await sut.StrikeAndCheckLimit(hash, itemName);
|
bool result = await sut.StrikeAndCheckLimit(hash, itemName, strikeType);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.ShouldBeTrue();
|
result.ShouldBeTrue();
|
||||||
|
|||||||
@@ -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 Domain.Enums;
|
||||||
using Infrastructure.Interceptors;
|
using Infrastructure.Interceptors;
|
||||||
using Infrastructure.Verticals.ContentBlocker;
|
using Infrastructure.Verticals.ContentBlocker;
|
||||||
using Infrastructure.Verticals.DownloadClient;
|
using Infrastructure.Verticals.DownloadClient;
|
||||||
@@ -45,6 +46,6 @@ public class TestDownloadService : DownloadService
|
|||||||
|
|
||||||
// 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);
|
||||||
public new Task<bool> StrikeAndCheckLimit(string hash, string itemName) => base.StrikeAndCheckLimit(hash, itemName);
|
public new Task<bool> StrikeAndCheckLimit(string hash, string itemName, StrikeType strikeType) => base.StrikeAndCheckLimit(hash, itemName, strikeType);
|
||||||
public new SeedingCheckResult ShouldCleanDownload(double ratio, TimeSpan seedingTime, Category category) => base.ShouldCleanDownload(ratio, seedingTime, category);
|
public new SeedingCheckResult ShouldCleanDownload(double ratio, TimeSpan seedingTime, Category category) => base.ShouldCleanDownload(ratio, seedingTime, category);
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<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.3" />
|
||||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||||
<PackageReference Include="MassTransit" Version="8.3.6" />
|
<PackageReference Include="MassTransit" Version="8.3.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
|
||||||
|
|||||||
@@ -43,9 +43,11 @@ public abstract class ArrClient : IArrClient
|
|||||||
|
|
||||||
public virtual async Task<QueueListResponse> GetQueueItemsAsync(ArrInstance arrInstance, int page)
|
public virtual async Task<QueueListResponse> GetQueueItemsAsync(ArrInstance arrInstance, int page)
|
||||||
{
|
{
|
||||||
Uri uri = new(arrInstance.Url, GetQueueUrlPath(page));
|
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||||
|
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/{GetQueueUrlPath().TrimStart('/')}";
|
||||||
|
uriBuilder.Query = GetQueueUrlQuery(page);
|
||||||
|
|
||||||
using HttpRequestMessage request = new(HttpMethod.Get, uri);
|
using HttpRequestMessage request = new(HttpMethod.Get, uriBuilder.Uri);
|
||||||
SetApiKey(request, arrInstance.ApiKey);
|
SetApiKey(request, arrInstance.ApiKey);
|
||||||
|
|
||||||
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||||
@@ -56,7 +58,7 @@ public abstract class ArrClient : IArrClient
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
_logger.LogError("queue list failed | {uri}", uri);
|
_logger.LogError("queue list failed | {uri}", uriBuilder.Uri);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +67,7 @@ public abstract class ArrClient : IArrClient
|
|||||||
|
|
||||||
if (queueResponse is null)
|
if (queueResponse is null)
|
||||||
{
|
{
|
||||||
throw new Exception($"unrecognized queue list response | {uri} | {responseBody}");
|
throw new Exception($"unrecognized queue list response | {uriBuilder.Uri} | {responseBody}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return queueResponse;
|
return queueResponse;
|
||||||
@@ -112,13 +114,20 @@ public abstract class ArrClient : IArrClient
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record, bool removeFromClient)
|
public virtual async Task DeleteQueueItemAsync(
|
||||||
|
ArrInstance arrInstance,
|
||||||
|
QueueRecord record,
|
||||||
|
bool removeFromClient,
|
||||||
|
DeleteReason deleteReason
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Uri uri = new(arrInstance.Url, GetQueueDeleteUrlPath(record.Id, removeFromClient));
|
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||||
|
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/{GetQueueDeleteUrlPath(record.Id).TrimStart('/')}";
|
||||||
|
uriBuilder.Query = GetQueueDeleteUrlQuery(removeFromClient);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using HttpRequestMessage request = new(HttpMethod.Delete, uri);
|
using HttpRequestMessage request = new(HttpMethod.Delete, uriBuilder.Uri);
|
||||||
SetApiKey(request, arrInstance.ApiKey);
|
SetApiKey(request, arrInstance.ApiKey);
|
||||||
|
|
||||||
HttpResponseMessage? response = await _dryRunInterceptor.InterceptAsync<HttpResponseMessage>(SendRequestAsync, request);
|
HttpResponseMessage? response = await _dryRunInterceptor.InterceptAsync<HttpResponseMessage>(SendRequestAsync, request);
|
||||||
@@ -126,15 +135,16 @@ public abstract class ArrClient : IArrClient
|
|||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
removeFromClient
|
removeFromClient
|
||||||
? "queue item deleted | {url} | {title}"
|
? "queue item deleted with reason {reason} | {url} | {title}"
|
||||||
: "queue item removed from arr | {url} | {title}",
|
: "queue item removed from arr with reason {reason} | {url} | {title}",
|
||||||
|
deleteReason.ToString(),
|
||||||
arrInstance.Url,
|
arrInstance.Url,
|
||||||
record.Title
|
record.Title
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
_logger.LogError("queue delete failed | {uri} | {title}", uri, record.Title);
|
_logger.LogError("queue delete failed | {uri} | {title}", uriBuilder.Uri, record.Title);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,9 +162,13 @@ public abstract class ArrClient : IArrClient
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract string GetQueueUrlPath(int page);
|
protected abstract string GetQueueUrlPath();
|
||||||
|
|
||||||
protected abstract string GetQueueDeleteUrlPath(long recordId, bool removeFromClient);
|
protected abstract string GetQueueUrlQuery(int page);
|
||||||
|
|
||||||
|
protected abstract string GetQueueDeleteUrlPath(long recordId);
|
||||||
|
|
||||||
|
protected abstract string GetQueueDeleteUrlQuery(bool removeFromClient);
|
||||||
|
|
||||||
protected virtual void SetApiKey(HttpRequestMessage request, string apiKey)
|
protected virtual void SetApiKey(HttpRequestMessage request, string apiKey)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public interface IArrClient
|
|||||||
|
|
||||||
Task<bool> ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload);
|
Task<bool> ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload);
|
||||||
|
|
||||||
Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record, bool removeFromClient);
|
Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record, bool removeFromClient, DeleteReason deleteReason);
|
||||||
|
|
||||||
Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items);
|
Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items);
|
||||||
|
|
||||||
|
|||||||
@@ -27,29 +27,42 @@ public class LidarrClient : ArrClient, ILidarrClient
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string GetQueueUrlPath(int page)
|
protected override string GetQueueUrlPath()
|
||||||
{
|
{
|
||||||
return $"/api/v1/queue?page={page}&pageSize=200&includeUnknownArtistItems=true&includeArtist=true&includeAlbum=true";
|
return "/api/v1/queue";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string GetQueueDeleteUrlPath(long recordId, bool removeFromClient)
|
protected override string GetQueueUrlQuery(int page)
|
||||||
{
|
{
|
||||||
string path = $"/api/v1/queue/{recordId}?blocklist=true&skipRedownload=true&changeCategory=false";
|
return $"page={page}&pageSize=200&includeUnknownArtistItems=true&includeArtist=true&includeAlbum=true";
|
||||||
|
}
|
||||||
|
|
||||||
path += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
protected override string GetQueueDeleteUrlPath(long recordId)
|
||||||
|
{
|
||||||
|
return $"/api/v1/queue/{recordId}";
|
||||||
|
}
|
||||||
|
|
||||||
return path;
|
protected override string GetQueueDeleteUrlQuery(bool removeFromClient)
|
||||||
|
{
|
||||||
|
string query = "blocklist=true&skipRedownload=true&changeCategory=false";
|
||||||
|
query += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||||
|
|
||||||
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
||||||
{
|
{
|
||||||
if (items?.Count is null or 0) return;
|
if (items?.Count is null or 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Uri uri = new(arrInstance.Url, "/api/v1/command");
|
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||||
|
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v1/command";
|
||||||
|
|
||||||
foreach (var command in GetSearchCommands(items))
|
foreach (var command in GetSearchCommands(items))
|
||||||
{
|
{
|
||||||
using HttpRequestMessage request = new(HttpMethod.Post, uri);
|
using HttpRequestMessage request = new(HttpMethod.Post, uriBuilder.Uri);
|
||||||
request.Content = new StringContent(
|
request.Content = new StringContent(
|
||||||
JsonConvert.SerializeObject(command, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
|
JsonConvert.SerializeObject(command, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
|
||||||
Encoding.UTF8,
|
Encoding.UTF8,
|
||||||
@@ -132,8 +145,11 @@ public class LidarrClient : ArrClient, ILidarrClient
|
|||||||
|
|
||||||
private async Task<List<Album>?> GetAlbumsAsync(ArrInstance arrInstance, List<long> albumIds)
|
private async Task<List<Album>?> GetAlbumsAsync(ArrInstance arrInstance, List<long> albumIds)
|
||||||
{
|
{
|
||||||
Uri uri = new(arrInstance.Url, $"api/v1/album?{string.Join('&', albumIds.Select(x => $"albumIds={x}"))}");
|
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||||
using HttpRequestMessage request = new(HttpMethod.Get, uri);
|
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v1/album";
|
||||||
|
uriBuilder.Query = string.Join('&', albumIds.Select(x => $"albumIds={x}"));
|
||||||
|
|
||||||
|
using HttpRequestMessage request = new(HttpMethod.Get, uriBuilder.Uri);
|
||||||
SetApiKey(request, arrInstance.ApiKey);
|
SetApiKey(request, arrInstance.ApiKey);
|
||||||
|
|
||||||
using var response = await _httpClient.SendAsync(request);
|
using var response = await _httpClient.SendAsync(request);
|
||||||
|
|||||||
@@ -27,18 +27,27 @@ public class RadarrClient : ArrClient, IRadarrClient
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string GetQueueUrlPath(int page)
|
protected override string GetQueueUrlPath()
|
||||||
{
|
{
|
||||||
return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownMovieItems=true&includeMovie=true";
|
return "/api/v3/queue";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string GetQueueDeleteUrlPath(long recordId, bool removeFromClient)
|
protected override string GetQueueUrlQuery(int page)
|
||||||
{
|
{
|
||||||
string path = $"/api/v3/queue/{recordId}?blocklist=true&skipRedownload=true&changeCategory=false";
|
return $"page={page}&pageSize=200&includeUnknownMovieItems=true&includeMovie=true";
|
||||||
|
}
|
||||||
path += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
|
||||||
|
|
||||||
return path;
|
protected override string GetQueueDeleteUrlPath(long recordId)
|
||||||
|
{
|
||||||
|
return $"/api/v3/queue/{recordId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GetQueueDeleteUrlQuery(bool removeFromClient)
|
||||||
|
{
|
||||||
|
string query = "blocklist=true&skipRedownload=true&changeCategory=false";
|
||||||
|
query += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||||
|
|
||||||
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
||||||
@@ -50,14 +59,16 @@ public class RadarrClient : ArrClient, IRadarrClient
|
|||||||
|
|
||||||
List<long> ids = items.Select(item => item.Id).ToList();
|
List<long> ids = items.Select(item => item.Id).ToList();
|
||||||
|
|
||||||
Uri uri = new(arrInstance.Url, "/api/v3/command");
|
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||||
|
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v3/command";
|
||||||
|
|
||||||
RadarrCommand command = new()
|
RadarrCommand command = new()
|
||||||
{
|
{
|
||||||
Name = "MoviesSearch",
|
Name = "MoviesSearch",
|
||||||
MovieIds = ids,
|
MovieIds = ids,
|
||||||
};
|
};
|
||||||
|
|
||||||
using HttpRequestMessage request = new(HttpMethod.Post, uri);
|
using HttpRequestMessage request = new(HttpMethod.Post, uriBuilder.Uri);
|
||||||
request.Content = new StringContent(
|
request.Content = new StringContent(
|
||||||
JsonConvert.SerializeObject(command),
|
JsonConvert.SerializeObject(command),
|
||||||
Encoding.UTF8,
|
Encoding.UTF8,
|
||||||
@@ -135,8 +146,10 @@ public class RadarrClient : ArrClient, IRadarrClient
|
|||||||
|
|
||||||
private async Task<Movie?> GetMovie(ArrInstance arrInstance, long movieId)
|
private async Task<Movie?> GetMovie(ArrInstance arrInstance, long movieId)
|
||||||
{
|
{
|
||||||
Uri uri = new(arrInstance.Url, $"api/v3/movie/{movieId}");
|
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||||
using HttpRequestMessage request = new(HttpMethod.Get, uri);
|
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v3/movie/{movieId}";
|
||||||
|
|
||||||
|
using HttpRequestMessage request = new(HttpMethod.Get, uriBuilder.Uri);
|
||||||
SetApiKey(request, arrInstance.ApiKey);
|
SetApiKey(request, arrInstance.ApiKey);
|
||||||
|
|
||||||
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||||
|
|||||||
@@ -28,18 +28,27 @@ public class SonarrClient : ArrClient, ISonarrClient
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string GetQueueUrlPath(int page)
|
protected override string GetQueueUrlPath()
|
||||||
{
|
{
|
||||||
return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownSeriesItems=true&includeSeries=true&includeEpisode=true";
|
return "/api/v3/queue";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string GetQueueDeleteUrlPath(long recordId, bool removeFromClient)
|
protected override string GetQueueUrlQuery(int page)
|
||||||
{
|
{
|
||||||
string path = $"/api/v3/queue/{recordId}?blocklist=true&skipRedownload=true&changeCategory=false";
|
return $"page={page}&pageSize=200&includeUnknownSeriesItems=true&includeSeries=true&includeEpisode=true";
|
||||||
|
}
|
||||||
|
|
||||||
path += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
protected override string GetQueueDeleteUrlPath(long recordId)
|
||||||
|
{
|
||||||
|
return $"/api/v3/queue/{recordId}";
|
||||||
|
}
|
||||||
|
|
||||||
return path;
|
protected override string GetQueueDeleteUrlQuery(bool removeFromClient)
|
||||||
|
{
|
||||||
|
string query = "blocklist=true&skipRedownload=true&changeCategory=false";
|
||||||
|
query += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||||
|
|
||||||
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
||||||
@@ -49,11 +58,12 @@ public class SonarrClient : ArrClient, ISonarrClient
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri uri = new(arrInstance.Url, "/api/v3/command");
|
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||||
|
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v3/command";
|
||||||
|
|
||||||
foreach (SonarrCommand command in GetSearchCommands(items.Cast<SonarrSearchItem>().ToHashSet()))
|
foreach (SonarrCommand command in GetSearchCommands(items.Cast<SonarrSearchItem>().ToHashSet()))
|
||||||
{
|
{
|
||||||
using HttpRequestMessage request = new(HttpMethod.Post, uri);
|
using HttpRequestMessage request = new(HttpMethod.Post, uriBuilder.Uri);
|
||||||
request.Content = new StringContent(
|
request.Content = new StringContent(
|
||||||
JsonConvert.SerializeObject(command, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
|
JsonConvert.SerializeObject(command, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
|
||||||
Encoding.UTF8,
|
Encoding.UTF8,
|
||||||
@@ -199,8 +209,11 @@ public class SonarrClient : ArrClient, ISonarrClient
|
|||||||
|
|
||||||
private async Task<List<Episode>?> GetEpisodesAsync(ArrInstance arrInstance, List<long> episodeIds)
|
private async Task<List<Episode>?> GetEpisodesAsync(ArrInstance arrInstance, List<long> episodeIds)
|
||||||
{
|
{
|
||||||
Uri uri = new(arrInstance.Url, $"api/v3/episode?{string.Join('&', episodeIds.Select(x => $"episodeIds={x}"))}");
|
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||||
using HttpRequestMessage request = new(HttpMethod.Get, uri);
|
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v3/episode";
|
||||||
|
uriBuilder.Query = string.Join('&', episodeIds.Select(x => $"episodeIds={x}"));
|
||||||
|
|
||||||
|
using HttpRequestMessage request = new(HttpMethod.Get, uriBuilder.Uri);
|
||||||
SetApiKey(request, arrInstance.ApiKey);
|
SetApiKey(request, arrInstance.ApiKey);
|
||||||
|
|
||||||
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||||
@@ -212,8 +225,10 @@ public class SonarrClient : ArrClient, ISonarrClient
|
|||||||
|
|
||||||
private async Task<Series?> GetSeriesAsync(ArrInstance arrInstance, long seriesId)
|
private async Task<Series?> GetSeriesAsync(ArrInstance arrInstance, long seriesId)
|
||||||
{
|
{
|
||||||
Uri uri = new(arrInstance.Url, $"api/v3/series/{seriesId}");
|
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||||
using HttpRequestMessage request = new(HttpMethod.Get, uri);
|
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v3/series/{seriesId}";
|
||||||
|
|
||||||
|
using HttpRequestMessage request = new(HttpMethod.Get, uriBuilder.Uri);
|
||||||
SetApiKey(request, arrInstance.ApiKey);
|
SetApiKey(request, arrInstance.ApiKey);
|
||||||
|
|
||||||
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
|
|
||||||
public override async Task ExecuteAsync()
|
public override async Task ExecuteAsync()
|
||||||
{
|
{
|
||||||
if (_downloadClientConfig.DownloadClient is Common.Enums.DownloadClient.None)
|
if (_downloadClientConfig.DownloadClient is Common.Enums.DownloadClient.None or Common.Enums.DownloadClient.Disabled)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("download client is set to none");
|
_logger.LogWarning("download client is not set");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
removeFromClient = false;
|
removeFromClient = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient);
|
await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient, DeleteReason.AllFilesBlocked);
|
||||||
await _notifier.NotifyQueueItemDeleted(removeFromClient, DeleteReason.AllFilesBlocked);
|
await _notifier.NotifyQueueItemDeleted(removeFromClient, DeleteReason.AllFilesBlocked);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ public sealed class DownloadCleaner : GenericHandler
|
|||||||
|
|
||||||
public override async Task ExecuteAsync()
|
public override async Task ExecuteAsync()
|
||||||
{
|
{
|
||||||
if (_downloadClientConfig.DownloadClient is Common.Enums.DownloadClient.None)
|
if (_downloadClientConfig.DownloadClient is Common.Enums.DownloadClient.None or Common.Enums.DownloadClient.Disabled)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("download client is set to none");
|
_logger.LogWarning("download client is not set");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ public sealed class DelugeClient
|
|||||||
public DelugeClient(IOptions<DelugeConfig> config, IHttpClientFactory httpClientFactory)
|
public DelugeClient(IOptions<DelugeConfig> config, IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
_config = config.Value;
|
_config = config.Value;
|
||||||
|
_config.Validate();
|
||||||
_httpClient = httpClientFactory.CreateClient(nameof(DelugeService));
|
_httpClient = httpClientFactory.CreateClient(nameof(DelugeService));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,11 +80,24 @@ public sealed class DelugeClient
|
|||||||
|
|
||||||
public async Task<TorrentStatus?> GetTorrentStatus(string hash)
|
public async Task<TorrentStatus?> GetTorrentStatus(string hash)
|
||||||
{
|
{
|
||||||
return await SendRequest<TorrentStatus?>(
|
try
|
||||||
"web.get_torrent_status",
|
{
|
||||||
hash,
|
return await SendRequest<TorrentStatus?>(
|
||||||
Fields
|
"web.get_torrent_status",
|
||||||
);
|
hash,
|
||||||
|
Fields
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (DelugeClientException e)
|
||||||
|
{
|
||||||
|
// Deluge returns an error when the torrent is not found
|
||||||
|
if (e.Message == "AttributeError: 'NoneType' object has no attribute 'call'")
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<TorrentStatus>?> GetStatusForAllTorrents()
|
public async Task<List<TorrentStatus>?> GetStatusForAllTorrents()
|
||||||
@@ -121,8 +135,12 @@ public sealed class DelugeClient
|
|||||||
{
|
{
|
||||||
StringContent content = new StringContent(json);
|
StringContent content = new StringContent(json);
|
||||||
content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/json");
|
content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/json");
|
||||||
|
|
||||||
var responseMessage = await _httpClient.PostAsync(new Uri(_config.Url, "/json"), content);
|
UriBuilder uriBuilder = new(_config.Url);
|
||||||
|
uriBuilder.Path = string.IsNullOrEmpty(_config.UrlBase)
|
||||||
|
? $"{uriBuilder.Path.TrimEnd('/')}/json"
|
||||||
|
: $"{uriBuilder.Path.TrimEnd('/')}/{_config.UrlBase.TrimStart('/').TrimEnd('/')}/json";
|
||||||
|
var responseMessage = await _httpClient.PostAsync(uriBuilder.Uri, content);
|
||||||
responseMessage.EnsureSuccessStatusCode();
|
responseMessage.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var responseJson = await responseMessage.Content.ReadAsStringAsync();
|
var responseJson = await responseMessage.Content.ReadAsStringAsync();
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ public class DelugeService : DownloadService, IDelugeService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.IsPrivate = download.Private;
|
||||||
|
|
||||||
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||||
@@ -79,6 +81,7 @@ public class DelugeService : DownloadService, IDelugeService
|
|||||||
{
|
{
|
||||||
_logger.LogDebug(exception, "failed to find torrent {hash} in the download client", hash);
|
_logger.LogDebug(exception, "failed to find torrent {hash} in the download client", hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool shouldRemove = contents?.Contents?.Count > 0;
|
bool shouldRemove = contents?.Contents?.Count > 0;
|
||||||
|
|
||||||
@@ -92,17 +95,15 @@ public class DelugeService : DownloadService, IDelugeService
|
|||||||
|
|
||||||
if (shouldRemove)
|
if (shouldRemove)
|
||||||
{
|
{
|
||||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
// remove if all files are unwanted
|
||||||
|
result.ShouldRemove = true;
|
||||||
|
result.DeleteReason = DeleteReason.AllFilesSkipped;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(download);
|
// remove if download is stuck
|
||||||
result.IsPrivate = download.Private;
|
(result.ShouldRemove, result.DeleteReason) = await IsItemStuckAndShouldRemove(download);
|
||||||
|
|
||||||
if (!shouldRemove && result.ShouldRemove)
|
|
||||||
{
|
|
||||||
result.DeleteReason = DeleteReason.Stalled;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,33 +296,33 @@ public class DelugeService : DownloadService, IDelugeService
|
|||||||
await _client.ChangeFilesPriority(hash, sortedPriorities);
|
await _client.ChangeFilesPriority(hash, sortedPriorities);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> IsItemStuckAndShouldRemove(TorrentStatus status)
|
private async Task<(bool, DeleteReason)> IsItemStuckAndShouldRemove(TorrentStatus status)
|
||||||
{
|
{
|
||||||
if (_queueCleanerConfig.StalledMaxStrikes is 0)
|
if (_queueCleanerConfig.StalledMaxStrikes is 0)
|
||||||
{
|
{
|
||||||
return false;
|
return (false, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_queueCleanerConfig.StalledIgnorePrivate && status.Private)
|
if (_queueCleanerConfig.StalledIgnorePrivate && status.Private)
|
||||||
{
|
{
|
||||||
// ignore private trackers
|
// ignore private trackers
|
||||||
_logger.LogDebug("skip stalled check | download is private | {name}", status.Name);
|
_logger.LogDebug("skip stalled check | download is private | {name}", status.Name);
|
||||||
return false;
|
return (false, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.State is null || !status.State.Equals("Downloading", StringComparison.InvariantCultureIgnoreCase))
|
if (status.State is null || !status.State.Equals("Downloading", StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
return false;
|
return (false, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.Eta > 0)
|
if (status.Eta > 0)
|
||||||
{
|
{
|
||||||
return false;
|
return (false, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
ResetStrikesOnProgress(status.Hash!, status.TotalDone);
|
ResetStrikesOnProgress(status.Hash!, status.TotalDone);
|
||||||
|
|
||||||
return await StrikeAndCheckLimit(status.Hash!, status.Name!);
|
return (await StrikeAndCheckLimit(status.Hash!, status.Name!, StrikeType.Stalled), DeleteReason.Stalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ProcessFiles(Dictionary<string, DelugeFileOrDirectory>? contents, Action<string, DelugeFileOrDirectory> processFile)
|
private static void ProcessFiles(Dictionary<string, DelugeFileOrDirectory>? contents, Action<string, DelugeFileOrDirectory> processFile)
|
||||||
|
|||||||
@@ -100,10 +100,11 @@ public abstract class DownloadService : IDownloadService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hash">The torrent hash.</param>
|
/// <param name="hash">The torrent hash.</param>
|
||||||
/// <param name="itemName">The name or title of the item.</param>
|
/// <param name="itemName">The name or title of the item.</param>
|
||||||
|
/// <param name="strikeType"></param>
|
||||||
/// <returns>True if the limit has been reached; otherwise, false.</returns>
|
/// <returns>True if the limit has been reached; otherwise, false.</returns>
|
||||||
protected async Task<bool> StrikeAndCheckLimit(string hash, string itemName)
|
protected async Task<bool> StrikeAndCheckLimit(string hash, string itemName, StrikeType strikeType)
|
||||||
{
|
{
|
||||||
return await _striker.StrikeAndCheckLimit(hash, itemName, _queueCleanerConfig.StalledMaxStrikes, StrikeType.Stalled);
|
return await _striker.StrikeAndCheckLimit(hash, itemName, _queueCleanerConfig.StalledMaxStrikes, strikeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected SeedingCheckResult ShouldCleanDownload(double ratio, TimeSpan seedingTime, Category category)
|
protected SeedingCheckResult ShouldCleanDownload(double ratio, TimeSpan seedingTime, Category category)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public sealed class DownloadServiceFactory
|
|||||||
Common.Enums.DownloadClient.Deluge => _serviceProvider.GetRequiredService<DelugeService>(),
|
Common.Enums.DownloadClient.Deluge => _serviceProvider.GetRequiredService<DelugeService>(),
|
||||||
Common.Enums.DownloadClient.Transmission => _serviceProvider.GetRequiredService<TransmissionService>(),
|
Common.Enums.DownloadClient.Transmission => _serviceProvider.GetRequiredService<TransmissionService>(),
|
||||||
Common.Enums.DownloadClient.None => _serviceProvider.GetRequiredService<DummyDownloadService>(),
|
Common.Enums.DownloadClient.None => _serviceProvider.GetRequiredService<DummyDownloadService>(),
|
||||||
|
Common.Enums.DownloadClient.Disabled => _serviceProvider.GetRequiredService<DummyDownloadService>(),
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,11 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
{
|
{
|
||||||
_config = config.Value;
|
_config = config.Value;
|
||||||
_config.Validate();
|
_config.Validate();
|
||||||
_client = new(httpClientFactory.CreateClient(Constants.HttpClientWithRetryName), _config.Url);
|
UriBuilder uriBuilder = new(_config.Url);
|
||||||
|
uriBuilder.Path = string.IsNullOrEmpty(_config.UrlBase)
|
||||||
|
? uriBuilder.Path
|
||||||
|
: $"{uriBuilder.Path.TrimEnd('/')}/{_config.UrlBase.TrimStart('/')}";
|
||||||
|
_client = new(httpClientFactory.CreateClient(Constants.HttpClientWithRetryName), uriBuilder.Uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task LoginAsync()
|
public override async Task LoginAsync()
|
||||||
@@ -92,30 +96,26 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
bool.TryParse(dictValue?.ToString(), out bool boolValue)
|
bool.TryParse(dictValue?.ToString(), out bool boolValue)
|
||||||
&& boolValue;
|
&& boolValue;
|
||||||
|
|
||||||
// if all files were blocked by qBittorrent
|
|
||||||
if (download is { CompletionOn: not null, Downloaded: null or 0 })
|
|
||||||
{
|
|
||||||
result.ShouldRemove = true;
|
|
||||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
|
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
|
||||||
|
|
||||||
// if all files are marked as skip
|
|
||||||
if (files?.Count is > 0 && files.All(x => x.Priority is TorrentContentPriority.Skip))
|
if (files?.Count is > 0 && files.All(x => x.Priority is TorrentContentPriority.Skip))
|
||||||
{
|
{
|
||||||
result.ShouldRemove = true;
|
result.ShouldRemove = true;
|
||||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
|
||||||
|
// if all files were blocked by qBittorrent
|
||||||
|
if (download is { CompletionOn: not null, Downloaded: null or 0 })
|
||||||
|
{
|
||||||
|
result.DeleteReason = DeleteReason.AllFilesSkippedByQBit;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove if all files are unwanted
|
||||||
|
result.DeleteReason = DeleteReason.AllFilesSkipped;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.ShouldRemove = await IsItemStuckAndShouldRemove(download, result.IsPrivate);
|
// remove if download is stuck
|
||||||
|
(result.ShouldRemove, result.DeleteReason) = await IsItemStuckAndShouldRemove(download, result.IsPrivate);
|
||||||
if (result.ShouldRemove)
|
|
||||||
{
|
|
||||||
result.DeleteReason = DeleteReason.Stalled;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -333,30 +333,35 @@ public class QBitService : DownloadService, IQBitService
|
|||||||
_client.Dispose();
|
_client.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> IsItemStuckAndShouldRemove(TorrentInfo torrent, bool isPrivate)
|
private async Task<(bool, DeleteReason)> IsItemStuckAndShouldRemove(TorrentInfo torrent, bool isPrivate)
|
||||||
{
|
{
|
||||||
if (_queueCleanerConfig.StalledMaxStrikes is 0)
|
if (_queueCleanerConfig.StalledMaxStrikes is 0)
|
||||||
{
|
{
|
||||||
return false;
|
return (false, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_queueCleanerConfig.StalledIgnorePrivate && isPrivate)
|
if (_queueCleanerConfig.StalledIgnorePrivate && isPrivate)
|
||||||
{
|
{
|
||||||
// ignore private trackers
|
// ignore private trackers
|
||||||
_logger.LogDebug("skip stalled check | download is private | {name}", torrent.Name);
|
_logger.LogDebug("skip stalled check | download is private | {name}", torrent.Name);
|
||||||
return false;
|
return (false, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (torrent.State is not TorrentState.StalledDownload and not TorrentState.FetchingMetadata
|
if (torrent.State is not TorrentState.StalledDownload and not TorrentState.FetchingMetadata
|
||||||
and not TorrentState.ForcedFetchingMetadata)
|
and not TorrentState.ForcedFetchingMetadata)
|
||||||
{
|
{
|
||||||
// ignore other states
|
// ignore other states
|
||||||
return false;
|
return (false, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
ResetStrikesOnProgress(torrent.Hash, torrent.Downloaded ?? 0);
|
ResetStrikesOnProgress(torrent.Hash, torrent.Downloaded ?? 0);
|
||||||
|
|
||||||
return await StrikeAndCheckLimit(torrent.Hash, torrent.Name);
|
if (torrent.State is TorrentState.StalledDownload)
|
||||||
|
{
|
||||||
|
return (await StrikeAndCheckLimit(torrent.Hash, torrent.Name, StrikeType.Stalled), DeleteReason.Stalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await StrikeAndCheckLimit(torrent.Hash, torrent.Name, StrikeType.DownloadingMetadata), DeleteReason.DownloadingMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IReadOnlyList<TorrentTracker>> GetTrackersAsync(string hash)
|
private async Task<IReadOnlyList<TorrentTracker>> GetTrackersAsync(string hash)
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
{
|
{
|
||||||
private readonly TransmissionConfig _config;
|
private readonly TransmissionConfig _config;
|
||||||
private readonly Client _client;
|
private readonly Client _client;
|
||||||
private TorrentInfo[]? _torrentsCache;
|
|
||||||
|
|
||||||
private static readonly string[] Fields =
|
private static readonly string[] Fields =
|
||||||
[
|
[
|
||||||
@@ -64,9 +63,13 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
{
|
{
|
||||||
_config = config.Value;
|
_config = config.Value;
|
||||||
_config.Validate();
|
_config.Validate();
|
||||||
|
UriBuilder uriBuilder = new(_config.Url);
|
||||||
|
uriBuilder.Path = string.IsNullOrEmpty(_config.UrlBase)
|
||||||
|
? $"{uriBuilder.Path.TrimEnd('/')}/rpc"
|
||||||
|
: $"{uriBuilder.Path.TrimEnd('/')}/{_config.UrlBase.TrimStart('/').TrimEnd('/')}/rpc";
|
||||||
_client = new(
|
_client = new(
|
||||||
httpClientFactory.CreateClient(Constants.HttpClientWithRetryName),
|
httpClientFactory.CreateClient(Constants.HttpClientWithRetryName),
|
||||||
new Uri(_config.Url, "/transmission/rpc").ToString(),
|
uriBuilder.Uri.ToString(),
|
||||||
login: _config.Username,
|
login: _config.Username,
|
||||||
password: _config.Password
|
password: _config.Password
|
||||||
);
|
);
|
||||||
@@ -115,17 +118,15 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
|
|
||||||
if (shouldRemove)
|
if (shouldRemove)
|
||||||
{
|
{
|
||||||
|
// remove if all files are unwanted
|
||||||
|
result.ShouldRemove = true;
|
||||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove if all files are unwanted or download is stuck
|
// remove if download is stuck
|
||||||
result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(download);
|
(result.ShouldRemove, result.DeleteReason) = await IsItemStuckAndShouldRemove(download);
|
||||||
|
|
||||||
if (!shouldRemove && result.ShouldRemove)
|
|
||||||
{
|
|
||||||
result.DeleteReason = DeleteReason.Stalled;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,60 +335,38 @@ public class TransmissionService : DownloadService, ITransmissionService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> IsItemStuckAndShouldRemove(TorrentInfo torrent)
|
private async Task<(bool, DeleteReason)> IsItemStuckAndShouldRemove(TorrentInfo torrent)
|
||||||
{
|
{
|
||||||
if (_queueCleanerConfig.StalledMaxStrikes is 0)
|
if (_queueCleanerConfig.StalledMaxStrikes is 0)
|
||||||
{
|
{
|
||||||
return false;
|
return (false, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_queueCleanerConfig.StalledIgnorePrivate && (torrent.IsPrivate ?? false))
|
if (_queueCleanerConfig.StalledIgnorePrivate && (torrent.IsPrivate ?? false))
|
||||||
{
|
{
|
||||||
// ignore private trackers
|
// ignore private trackers
|
||||||
_logger.LogDebug("skip stalled check | download is private | {name}", torrent.Name);
|
_logger.LogDebug("skip stalled check | download is private | {name}", torrent.Name);
|
||||||
return false;
|
return (false, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (torrent.Status is not 4)
|
if (torrent.Status is not 4)
|
||||||
{
|
{
|
||||||
// not in downloading state
|
// not in downloading state
|
||||||
return false;
|
return (false, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (torrent.Eta > 0)
|
if (torrent.Eta > 0)
|
||||||
{
|
{
|
||||||
return false;
|
return (false, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
ResetStrikesOnProgress(torrent.HashString!, torrent.DownloadedEver ?? 0);
|
ResetStrikesOnProgress(torrent.HashString!, torrent.DownloadedEver ?? 0);
|
||||||
|
|
||||||
return await StrikeAndCheckLimit(torrent.HashString!, torrent.Name!);
|
return (await StrikeAndCheckLimit(torrent.HashString!, torrent.Name!, StrikeType.Stalled), DeleteReason.Stalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TorrentInfo?> GetTorrentAsync(string hash)
|
private async Task<TorrentInfo?> GetTorrentAsync(string hash) =>
|
||||||
{
|
(await _client.TorrentGetAsync(Fields, hash))
|
||||||
TorrentInfo? torrent = _torrentsCache?
|
?.Torrents
|
||||||
.FirstOrDefault(x => x.HashString.Equals(hash, StringComparison.InvariantCultureIgnoreCase));
|
?.FirstOrDefault();
|
||||||
|
|
||||||
if (_torrentsCache is null || torrent is null)
|
|
||||||
{
|
|
||||||
// refresh cache
|
|
||||||
_torrentsCache = (await _client.TorrentGetAsync(Fields))
|
|
||||||
?.Torrents;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_torrentsCache?.Length is null or 0)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("could not list torrents | {url}", _config.Url);
|
|
||||||
}
|
|
||||||
|
|
||||||
torrent = _torrentsCache?.FirstOrDefault(x => x.HashString.Equals(hash, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
|
|
||||||
if (torrent is null)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("could not find torrent | {hash} | {url}", hash, _config.Url);
|
|
||||||
}
|
|
||||||
|
|
||||||
return torrent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ public sealed class QueueCleaner : GenericHandler
|
|||||||
|
|
||||||
StalledResult stalledCheckResult = new();
|
StalledResult stalledCheckResult = new();
|
||||||
|
|
||||||
if (record.Protocol is "torrent")
|
if (record.Protocol is "torrent" && _downloadClientConfig.DownloadClient is not Common.Enums.DownloadClient.Disabled)
|
||||||
{
|
{
|
||||||
if (_downloadClientConfig.DownloadClient is Common.Enums.DownloadClient.None)
|
if (_downloadClientConfig.DownloadClient is Common.Enums.DownloadClient.None)
|
||||||
{
|
{
|
||||||
@@ -132,7 +132,7 @@ public sealed class QueueCleaner : GenericHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient);
|
await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient, deleteReason);
|
||||||
await _notifier.NotifyQueueItemDeleted(removeFromClient, deleteReason);
|
await _notifier.NotifyQueueItemDeleted(removeFromClient, deleteReason);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+28
-5
@@ -135,7 +135,7 @@
|
|||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
> [!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 private 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.
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Setting `QUEUECLEANER__STALLED_DELETE_PRIVATE=true` means you don't care about seeding, ratio, H&R and potentially losing your tracker account.
|
> Setting `QUEUECLEANER__STALLED_DELETE_PRIVATE=true` means you don't care about seeding, ratio, H&R and potentially losing your private tracker account.
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Setting `CONTENTBLOCKER__DELETE_PRIVATE=true` means you don't care about seeding, ratio, H&R and potentially losing your tracker account.
|
> Setting `CONTENTBLOCKER__DELETE_PRIVATE=true` means you don't care about seeding, ratio, H&R and potentially losing your private tracker account.
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@
|
|||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
> [!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 private tracker account.
|
||||||
|
|
||||||
#### **`DOWNLOADCLEANER__CATEGORIES__0__NAME`**
|
#### **`DOWNLOADCLEANER__CATEGORIES__0__NAME`**
|
||||||
- Name of the category to clean.
|
- Name of the category to clean.
|
||||||
@@ -360,19 +360,30 @@
|
|||||||
#### **`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`, `disabled`.
|
||||||
- Default: `none`
|
- Default: `none`
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
> [!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.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> When the download client is set to `disabled`, the queue cleaner will be able to remove items that are failed to be imported even if there is no download client configured. This means that all downloads, including private ones, will be completely removed.
|
||||||
|
>
|
||||||
|
> Setting `DOWNLOAD_CLIENT=disabled` means you don't care about seeding, ratio, H&R and potentially losing your private tracker account.
|
||||||
|
|
||||||
#### **`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__URL_BASE`**
|
||||||
|
- Adds a prefix to the qBittorrent url, such as `[QBITTORRENT__URL]/[QBITTORRENT__URL_BASE]/api`.
|
||||||
|
- Type: String.
|
||||||
|
- Default: Empty.
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
#### **`QBITTORRENT__USERNAME`**
|
#### **`QBITTORRENT__USERNAME`**
|
||||||
- Username for qBittorrent authentication.
|
- Username for qBittorrent authentication.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
@@ -391,6 +402,12 @@
|
|||||||
- Default: `http://localhost:8112`.
|
- Default: `http://localhost:8112`.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`DELUGE__URL_BASE`**
|
||||||
|
- Adds a prefix to the deluge json url, such as `[DELUGE__URL]/[DELUGE__URL_BASE]/json`.
|
||||||
|
- Type: String.
|
||||||
|
- Default: Empty.
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
#### **`DELUGE__PASSWORD`**
|
#### **`DELUGE__PASSWORD`**
|
||||||
- Password for Deluge authentication.
|
- Password for Deluge authentication.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
@@ -403,6 +420,12 @@
|
|||||||
- Default: `http://localhost:9091`.
|
- Default: `http://localhost:9091`.
|
||||||
- Required: No.
|
- Required: No.
|
||||||
|
|
||||||
|
#### **`TRANSMISSION__URL_BASE`**
|
||||||
|
- Adds a prefix to the Transmission rpc url, such as `[TRANSMISSION__URL]/[TRANSMISSION__URL_BASE]/rpc`.
|
||||||
|
- Type: String.
|
||||||
|
- Default: `transmission`.
|
||||||
|
- Required: No.
|
||||||
|
|
||||||
#### **`TRANSMISSION__USERNAME`**
|
#### **`TRANSMISSION__USERNAME`**
|
||||||
- Username for Transmission authentication.
|
- Username for Transmission authentication.
|
||||||
- Type: String.
|
- Type: String.
|
||||||
|
|||||||
Reference in New Issue
Block a user