initial project commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Common.Configuration;
|
||||||
|
|
||||||
|
public sealed class QBitConfig
|
||||||
|
{
|
||||||
|
public required Uri Url { get; set; }
|
||||||
|
|
||||||
|
public required string Username { get; set; }
|
||||||
|
|
||||||
|
public required string Password { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Common.Configuration;
|
||||||
|
|
||||||
|
public sealed class QuartzConfig
|
||||||
|
{
|
||||||
|
public required string FrozenTorrentTrigger { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Common.Configuration;
|
||||||
|
|
||||||
|
public sealed class SonarrConfig
|
||||||
|
{
|
||||||
|
public required List<SonarrInstance> Instances { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Common.Configuration;
|
||||||
|
|
||||||
|
public sealed class SonarrInstance
|
||||||
|
{
|
||||||
|
public required Uri Url { get; set; }
|
||||||
|
|
||||||
|
public required string ApiKey { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Domain.Sonarr.Queue;
|
||||||
|
|
||||||
|
public record CustomFormat(
|
||||||
|
int Id,
|
||||||
|
string Name
|
||||||
|
);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Domain.Sonarr.Queue;
|
||||||
|
|
||||||
|
public record Language(
|
||||||
|
int Id,
|
||||||
|
string Name
|
||||||
|
);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Domain.Sonarr.Queue;
|
||||||
|
|
||||||
|
public record QueueListResponse(
|
||||||
|
int Page,
|
||||||
|
int PageSize,
|
||||||
|
string SortKey,
|
||||||
|
string SortDirection,
|
||||||
|
int TotalRecords,
|
||||||
|
IReadOnlyList<Record> Records
|
||||||
|
);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace Domain.Sonarr.Queue;
|
||||||
|
|
||||||
|
public record Record(
|
||||||
|
int SeriesId,
|
||||||
|
int EpisodeId,
|
||||||
|
int SeasonNumber,
|
||||||
|
IReadOnlyList<Language> Languages,
|
||||||
|
IReadOnlyList<CustomFormat> CustomFormats,
|
||||||
|
int CustomFormatScore,
|
||||||
|
int Size,
|
||||||
|
string Title,
|
||||||
|
int Sizeleft,
|
||||||
|
string Timeleft,
|
||||||
|
DateTime EstimatedCompletionTime,
|
||||||
|
DateTime Added,
|
||||||
|
string Status,
|
||||||
|
string TrackedDownloadStatus,
|
||||||
|
string TrackedDownloadState,
|
||||||
|
IReadOnlyList<StatusMessage> StatusMessages,
|
||||||
|
string DownloadId,
|
||||||
|
string Protocol,
|
||||||
|
string DownloadClient,
|
||||||
|
bool DownloadClientHasPostImportCategory,
|
||||||
|
string Indexer,
|
||||||
|
string OutputPath,
|
||||||
|
bool EpisodeHasFile,
|
||||||
|
int Id
|
||||||
|
);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Domain.Sonarr.Queue;
|
||||||
|
|
||||||
|
public record Revision(
|
||||||
|
int Version,
|
||||||
|
int Real,
|
||||||
|
bool IsRepack
|
||||||
|
);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Domain.Sonarr.Queue;
|
||||||
|
|
||||||
|
public record StatusMessage(
|
||||||
|
string Title,
|
||||||
|
IReadOnlyList<string> Messages
|
||||||
|
);
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using Common.Configuration;
|
||||||
|
using Executable.Jobs;
|
||||||
|
using Infrastructure.Verticals.FrozenTorrent;
|
||||||
|
|
||||||
|
namespace Executable;
|
||||||
|
using Quartz;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) =>
|
||||||
|
services
|
||||||
|
.AddLogging(builder => builder.AddConsole())
|
||||||
|
.AddHttpClient()
|
||||||
|
.AddConfiguration(configuration)
|
||||||
|
.AddServices()
|
||||||
|
.AddQuartzServices(configuration);
|
||||||
|
|
||||||
|
private static IServiceCollection AddConfiguration(this IServiceCollection services, IConfiguration configuration) =>
|
||||||
|
services
|
||||||
|
.Configure<QuartzConfig>(configuration.GetSection(nameof(QuartzConfig)))
|
||||||
|
.Configure<SonarrConfig>(configuration.GetSection(nameof(SonarrConfig)));
|
||||||
|
|
||||||
|
private static IServiceCollection AddServices(this IServiceCollection services) =>
|
||||||
|
services
|
||||||
|
.AddTransient<FrozenTorrentHandler>();
|
||||||
|
|
||||||
|
private static IServiceCollection AddQuartzServices(this IServiceCollection services, IConfiguration configuration) =>
|
||||||
|
services
|
||||||
|
.AddQuartz(q =>
|
||||||
|
{
|
||||||
|
QuartzConfig? config = configuration.GetRequiredSection(nameof(QuartzConfig)).Get<QuartzConfig>();
|
||||||
|
|
||||||
|
if (config is null)
|
||||||
|
{
|
||||||
|
throw new NullReferenceException("Quartz configuration is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
q.AddFrozenTorrentJob(config.FrozenTorrentTrigger);
|
||||||
|
})
|
||||||
|
.AddQuartzHostedService(opt =>
|
||||||
|
{
|
||||||
|
opt.WaitForJobsToComplete = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
private static void AddFrozenTorrentJob(this IServiceCollectionQuartzConfigurator q, string trigger)
|
||||||
|
{
|
||||||
|
q.AddJob<FrozenTorrentJob>(opts =>
|
||||||
|
{
|
||||||
|
opts.WithIdentity(nameof(FrozenTorrentJob));
|
||||||
|
});
|
||||||
|
|
||||||
|
q.AddTrigger(opts =>
|
||||||
|
{
|
||||||
|
opts.ForJob(nameof(FrozenTorrentJob))
|
||||||
|
.WithIdentity($"{nameof(FrozenTorrentJob)}-trigger")
|
||||||
|
.WithCronSchedule(trigger);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UserSecretsId>dotnet-Executable-6108b2ba-f035-47bc-addf-aaf5e20da4b8</UserSecretsId>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0-rc.1.24431.7"/>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0-rc.2.24473.5" />
|
||||||
|
<PackageReference Include="Quartz" Version="3.13.1" />
|
||||||
|
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.13.1" />
|
||||||
|
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.13.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
|
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Infrastructure.Verticals.FrozenTorrent;
|
||||||
|
using Quartz;
|
||||||
|
|
||||||
|
namespace Executable.Jobs;
|
||||||
|
|
||||||
|
[DisallowConcurrentExecution]
|
||||||
|
public sealed class FrozenTorrentJob : IJob
|
||||||
|
{
|
||||||
|
private ILogger<FrozenTorrentJob> _logger;
|
||||||
|
private FrozenTorrentHandler _handler;
|
||||||
|
|
||||||
|
public FrozenTorrentJob(ILogger<FrozenTorrentJob> logger, FrozenTorrentHandler handler)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Execute(IJobExecutionContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _handler.HandleAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"{nameof(FrozenTorrentJob)} failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Executable;
|
||||||
|
|
||||||
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.AddInfrastructure(builder.Configuration);
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
host.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Warning",
|
||||||
|
"Quartz": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"QuartzConfig": {
|
||||||
|
"FrozenTorrentTrigger": "0 0/5 * * * ?"
|
||||||
|
},
|
||||||
|
"QBitConfig": {
|
||||||
|
"Url": "http://localhost:8080",
|
||||||
|
"Username": "",
|
||||||
|
"Password": ""
|
||||||
|
},
|
||||||
|
"SonarrConfig": [
|
||||||
|
{
|
||||||
|
"Url": "http://localhost:8989",
|
||||||
|
"ApiKey": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
|
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.1" />
|
||||||
|
<PackageReference Include="QBittorrent.Client" Version="1.9.24285.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using Common.Configuration;
|
||||||
|
using Domain.Sonarr.Queue;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using QBittorrent.Client;
|
||||||
|
|
||||||
|
namespace Infrastructure.Verticals.FrozenTorrent;
|
||||||
|
|
||||||
|
public sealed class FrozenTorrentHandler
|
||||||
|
{
|
||||||
|
private readonly ILogger<FrozenTorrentHandler> _logger;
|
||||||
|
private readonly QBitConfig _qBitConfig;
|
||||||
|
private readonly SonarrConfig _sonarrConfig;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
private const string SonarListUriTemplate = "/api/v3/queue?page={0}&pageSize=200&sortKey=timeleft";
|
||||||
|
private const string SonarDeleteUriTemplate = "/api/v3/queue/{0}?removeFromClient=true&blocklist=true&skipRedownload=true&changeCategory=false";
|
||||||
|
|
||||||
|
public FrozenTorrentHandler(
|
||||||
|
ILogger<FrozenTorrentHandler> logger,
|
||||||
|
IOptions<QBitConfig> qBitConfig,
|
||||||
|
IOptions<SonarrConfig> sonarrConfig,
|
||||||
|
IHttpClientFactory httpClientFactory)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_qBitConfig = qBitConfig.Value;
|
||||||
|
_sonarrConfig = sonarrConfig.Value;
|
||||||
|
_httpClient = httpClientFactory.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleAsync()
|
||||||
|
{
|
||||||
|
QBittorrentClient qBitClient = new(_qBitConfig.Url);
|
||||||
|
|
||||||
|
await qBitClient.LoginAsync(_qBitConfig.Username, _qBitConfig.Password);
|
||||||
|
|
||||||
|
foreach (SonarrInstance sonarrInstance in _sonarrConfig.Instances)
|
||||||
|
{
|
||||||
|
ushort page = 1;
|
||||||
|
int totalRecords = 0;
|
||||||
|
int processedRecords = 0;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
UriBuilder uriBuilder = new UriBuilder(sonarrInstance.Url);
|
||||||
|
uriBuilder.Path = string.Format(SonarListUriTemplate, page);
|
||||||
|
|
||||||
|
HttpRequestMessage sonarrRequest = new(HttpMethod.Get, uriBuilder.Uri);
|
||||||
|
sonarrRequest.Headers.Add("x-api-key", sonarrInstance.ApiKey);
|
||||||
|
|
||||||
|
HttpResponseMessage response = await _httpClient.SendAsync(sonarrRequest);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
string responseBody = await response.Content.ReadAsStringAsync();
|
||||||
|
QueueListResponse? queueResponse = JsonConvert.DeserializeObject<QueueListResponse>(responseBody);
|
||||||
|
|
||||||
|
if (queueResponse is null)
|
||||||
|
{
|
||||||
|
throw new Exception($"Failed to process response body:{responseBody}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Record record in queueResponse.Records)
|
||||||
|
{
|
||||||
|
var torrent = (await qBitClient.GetTorrentListAsync(new TorrentListQuery { Hashes = [record.DownloadId] }))
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (torrent is not { CompletionOn: not null, Downloaded: null or 0 })
|
||||||
|
{
|
||||||
|
// TODO log skip
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete and block from sonarr
|
||||||
|
uriBuilder = new(sonarrInstance.Url);
|
||||||
|
uriBuilder.Path = string.Format(SonarDeleteUriTemplate, record.Id);
|
||||||
|
|
||||||
|
sonarrRequest = new(HttpMethod.Delete, uriBuilder.Uri);
|
||||||
|
sonarrRequest.Headers.Add("x-api-key", sonarrInstance.ApiKey);
|
||||||
|
|
||||||
|
response = await _httpClient.SendAsync(sonarrRequest);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queueResponse.Records.Count is 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalRecords is 0)
|
||||||
|
{
|
||||||
|
totalRecords = queueResponse.TotalRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
processedRecords += queueResponse.Records.Count;
|
||||||
|
|
||||||
|
if (processedRecords >= totalRecords)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
page++;
|
||||||
|
} while (processedRecords < totalRecords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Executable", "Executable\Executable.csproj", "{38261017-0049-4377-B30F-7279CC2539B2}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{7AEA44C2-BDAA-49D8-A961-1FCA7779B39B}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{3FEC04CA-8CC5-49F5-BBDE-1581C7C8AFB5}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "Domain\Domain.csproj", "{8871592A-B260-4B15-8EF8-6AB24480DE5D}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{38261017-0049-4377-B30F-7279CC2539B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{38261017-0049-4377-B30F-7279CC2539B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{38261017-0049-4377-B30F-7279CC2539B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{38261017-0049-4377-B30F-7279CC2539B2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7AEA44C2-BDAA-49D8-A961-1FCA7779B39B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7AEA44C2-BDAA-49D8-A961-1FCA7779B39B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7AEA44C2-BDAA-49D8-A961-1FCA7779B39B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7AEA44C2-BDAA-49D8-A961-1FCA7779B39B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3FEC04CA-8CC5-49F5-BBDE-1581C7C8AFB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{3FEC04CA-8CC5-49F5-BBDE-1581C7C8AFB5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3FEC04CA-8CC5-49F5-BBDE-1581C7C8AFB5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{3FEC04CA-8CC5-49F5-BBDE-1581C7C8AFB5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{8871592A-B260-4B15-8EF8-6AB24480DE5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{8871592A-B260-4B15-8EF8-6AB24480DE5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{8871592A-B260-4B15-8EF8-6AB24480DE5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{8871592A-B260-4B15-8EF8-6AB24480DE5D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
Reference in New Issue
Block a user