Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75492a5792 | |||
| 5ca717d7e0 | |||
| 7068ee5e5a | |||
| 9f770473e5 | |||
| 5fe0f5750a | |||
| b8ce225ccc | |||
| f21f7388b7 | |||
| a1354f231a |
@@ -95,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.
|
||||||
@@ -214,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
|
||||||
|
|
||||||
@@ -268,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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
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; }
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -88,8 +88,6 @@ public sealed class QueueCleaner : GenericHandler
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogTrace("processing | {title} | {id}", record.Title, record.DownloadId);
|
|
||||||
|
|
||||||
// push record to context
|
// push record to context
|
||||||
ContextProvider.Set(nameof(QueueRecord), record);
|
ContextProvider.Set(nameof(QueueRecord), record);
|
||||||
|
|
||||||
@@ -134,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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -378,6 +378,12 @@
|
|||||||
- 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.
|
||||||
@@ -396,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.
|
||||||
@@ -408,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