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