45 Commits

Author SHA1 Message Date
pcjones
4ef636c781 Update README.md 2025-04-14 15:29:44 +02:00
pcjones
344751c7f3 Update README.md (#60) 2025-01-22 19:45:22 +01:00
Jonas F
d15b9e2e90 Update run_on_seedbox.sh 2025-01-14 01:02:56 +01:00
Jonas F
30fad063b6 Update run_on_seedbox.sh 2025-01-14 01:00:56 +01:00
Jonas F
eeff05783e Create run_on_seedbox.sh 2025-01-14 01:00:26 +01:00
pcjones
37673f8a6c Fix wrong check for empty api key again -_- 2025-01-13 23:14:10 +01:00
pcjones
d2eaac7a6c Fix wrong check for empty API key 2025-01-13 23:09:24 +01:00
pcjones
aa3765bcf2 Fix Proxy not working if no api key was set 2025-01-13 23:01:14 +01:00
pcjones
e81a956cc4 Add missing curly bracket 2025-01-13 21:53:05 +01:00
pcjones
e7f838cd61 Merge branch 'develop' 2025-01-13 21:29:47 +01:00
Jonas F
3764991e63 Merge Develop in master (#57) (#58)
* Merge master in develop (#55)

* Fix reachable and IP leak test (#44)

* Fix reachable check

Fixes failing reachable checks when Basic Authentication is enabled in
Sonarr, Radarr, etc.

* Add option to disable IP leak test

* Revert "Fix reachable and IP leak test (#44)" (#46)

This reverts commit 3f5d7bbef3.

* Release 0.6.1 (#48)

* Fix typo

* Fix typos

* Fix typos

* Fix typo

* Clarify error message

* Fix reachable and ipleak test (#47)

* Fix reachable check

Fixes failing reachable checks when Basic Authentication is enabled in
Sonarr, Radarr, etc.

* Add option to disable IP leak test

---------



* Add IpLeakTest environment variable to docker compose

---------



* Create Dockerfile.arm64

---------



* Add configurable cache duration

* Make proxy port configurable

* Make proxy port configurable

* Add API Key auth

* Add default settings to appsettings

* Fix too many Unauthorized access attempt warnings

---------

Co-authored-by: akuntsch <github@akuntsch.de>
2025-01-13 21:28:34 +01:00
Jonas F
d2a3963006 Merge Develop in master (#57)
* Merge master in develop (#55)

* Fix reachable and IP leak test (#44)

* Fix reachable check

Fixes failing reachable checks when Basic Authentication is enabled in
Sonarr, Radarr, etc.

* Add option to disable IP leak test

* Revert "Fix reachable and IP leak test (#44)" (#46)

This reverts commit 3f5d7bbef3.

* Release 0.6.1 (#48)

* Fix typo

* Fix typos

* Fix typos

* Fix typo

* Clarify error message

* Fix reachable and ipleak test (#47)

* Fix reachable check

Fixes failing reachable checks when Basic Authentication is enabled in
Sonarr, Radarr, etc.

* Add option to disable IP leak test

---------

Co-authored-by: Jonas F <github@pcjones.de>

* Add IpLeakTest environment variable to docker compose

---------

Co-authored-by: akuntsch <github@akuntsch.de>

* Create Dockerfile.arm64

---------

Co-authored-by: akuntsch <github@akuntsch.de>

* Add configurable cache duration

* Make proxy port configurable

* Make proxy port configurable

* Add API Key auth

* Add default settings to appsettings

* Fix too many Unauthorized access attempt warnings

---------

Co-authored-by: akuntsch <github@akuntsch.de>
2025-01-13 21:28:01 +01:00
Jonas F
270458a2a3 Merge branch 'master' into develop 2025-01-13 21:27:51 +01:00
pcjones
e3d4222f16 Merge branch 'develop' of https://github.com/PCJones/UmlautAdaptarr into develop 2025-01-13 21:26:28 +01:00
pcjones
ed044e9a59 Fix too many Unauthorized access attempt warnings 2025-01-13 21:26:24 +01:00
Jonas F
9cdf1950c6 Mergter (#56)
* Merge master in develop (#55)

* Fix reachable and IP leak test (#44)

* Fix reachable check

Fixes failing reachable checks when Basic Authentication is enabled in
Sonarr, Radarr, etc.

* Add option to disable IP leak test

* Revert "Fix reachable and IP leak test (#44)" (#46)

This reverts commit 3f5d7bbef3.

* Release 0.6.1 (#48)

* Fix typo

* Fix typos

* Fix typos

* Fix typo

* Clarify error message

* Fix reachable and ipleak test (#47)

* Fix reachable check

Fixes failing reachable checks when Basic Authentication is enabled in
Sonarr, Radarr, etc.

* Add option to disable IP leak test

---------

Co-authored-by: Jonas F <github@pcjones.de>

* Add IpLeakTest environment variable to docker compose

---------

Co-authored-by: akuntsch <github@akuntsch.de>

* Create Dockerfile.arm64

---------

Co-authored-by: akuntsch <github@akuntsch.de>

* Add configurable cache duration

* Make proxy port configurable

* Make proxy port configurable

* Add API Key auth

* Add default settings to appsettings

---------

Co-authored-by: akuntsch <github@akuntsch.de>
2025-01-13 21:20:03 +01:00
Jonas F
5463794a4f Merge branch 'master' into develop 2025-01-13 21:19:45 +01:00
pcjones
dd6b4c9d3b Add default settings to appsettings 2025-01-13 21:16:16 +01:00
pcjones
02a6ec2548 Add API Key auth 2025-01-13 21:14:31 +01:00
pcjones
275f29ec11 Make proxy port configurable 2025-01-13 19:35:30 +01:00
pcjones
f916aa3761 Make proxy port configurable 2025-01-13 19:35:20 +01:00
pcjones
b6390c15a1 Add configurable cache duration 2025-01-13 19:00:42 +01:00
Jonas F
e117826c6a Merge master in develop (#55)
* Fix reachable and IP leak test (#44)

* Fix reachable check

Fixes failing reachable checks when Basic Authentication is enabled in
Sonarr, Radarr, etc.

* Add option to disable IP leak test

* Revert "Fix reachable and IP leak test (#44)" (#46)

This reverts commit 3f5d7bbef3.

* Release 0.6.1 (#48)

* Fix typo

* Fix typos

* Fix typos

* Fix typo

* Clarify error message

* Fix reachable and ipleak test (#47)

* Fix reachable check

Fixes failing reachable checks when Basic Authentication is enabled in
Sonarr, Radarr, etc.

* Add option to disable IP leak test

---------

Co-authored-by: Jonas F <github@pcjones.de>

* Add IpLeakTest environment variable to docker compose

---------

Co-authored-by: akuntsch <github@akuntsch.de>

* Create Dockerfile.arm64

---------

Co-authored-by: akuntsch <github@akuntsch.de>
2025-01-13 18:49:26 +01:00
Jonas F
83905622cb Merge branch 'develop' into master 2025-01-13 18:48:49 +01:00
Jonas F
9207d6ec7c Create Dockerfile.arm64 2025-01-02 15:44:07 +01:00
pcjones
17456c6f90 Clarify error message 2024-11-08 13:56:29 +01:00
pcjones
c581233dbf Bypass domain check for localhost 2024-11-02 15:51:05 +01:00
pcjones
6fc399131b Allow port in URL 2024-11-02 15:46:52 +01:00
pcjones
31ac409d41 Fix logger 2024-10-25 13:37:50 +02:00
pcjones
03b50a24fd Log content on error at ProcessContent 2024-10-25 13:26:43 +02:00
PCJones
7ed68f2b84 Fix typo 2024-10-22 10:33:37 +02:00
pcjones
65847f34bc Fix missing semicolon 2024-10-21 23:34:12 +02:00
pcjones
29da771484 Add more logging to reachable check 2024-10-21 23:32:18 +02:00
Jonas F
cf3a5ab68a Release 0.6.1 (#48)
* Fix typo

* Fix typos

* Fix typos

* Fix typo

* Clarify error message

* Fix reachable and ipleak test (#47)

* Fix reachable check

Fixes failing reachable checks when Basic Authentication is enabled in
Sonarr, Radarr, etc.

* Add option to disable IP leak test

---------

Co-authored-by: Jonas F <github@pcjones.de>

* Add IpLeakTest environment variable to docker compose

---------

Co-authored-by: akuntsch <github@akuntsch.de>
2024-10-21 17:28:31 +02:00
pcjones
b8a1c64039 Add IpLeakTest environment variable to docker compose 2024-10-21 17:20:28 +02:00
pcjones
4ffdf9f53a Merge branch 'develop' of https://github.com/PCJones/UmlautAdaptarr into develop 2024-10-21 17:16:17 +02:00
akuntsch
4c582c7a6c Fix reachable and ipleak test (#47)
* Fix reachable check

Fixes failing reachable checks when Basic Authentication is enabled in
Sonarr, Radarr, etc.

* Add option to disable IP leak test

---------

Co-authored-by: Jonas F <github@pcjones.de>
2024-10-21 17:15:11 +02:00
Jonas F
46e1baf53c Revert "Fix reachable and IP leak test (#44)" (#46)
This reverts commit 3f5d7bbef3.
2024-10-21 14:26:55 +02:00
akuntsch
3f5d7bbef3 Fix reachable and IP leak test (#44)
* Fix reachable check

Fixes failing reachable checks when Basic Authentication is enabled in
Sonarr, Radarr, etc.

* Add option to disable IP leak test
2024-10-21 14:26:35 +02:00
pcjones
e95d18ed91 Clarify error message 2024-10-21 14:25:03 +02:00
pcjones
95f5054829 Merge branch 'develop' of https://github.com/PCJones/UmlautAdaptarr into develop 2024-10-12 14:24:43 +02:00
pcjones
2085a28da2 Fix typo 2024-09-30 14:10:56 +02:00
pcjones
0e38d5a0f3 Fix typos 2024-09-30 14:09:46 +02:00
pcjones
ee329c23e5 Fix typos 2024-09-30 14:09:32 +02:00
pcjones
c9ea74267b Fix typo 2024-09-30 14:09:01 +02:00
19 changed files with 381 additions and 99 deletions

11
Dockerfile.arm64 Normal file
View File

@@ -0,0 +1,11 @@
FROM --platform=linux/arm64 mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
WORKDIR /app
COPY . ./
RUN dotnet restore
RUN dotnet publish -c Release -o out
FROM --platform=linux/arm64 mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "UmlautAdaptarr.dll"]

View File

@@ -44,7 +44,10 @@ Einige Beispiele finden sich [weiter unten](https://github.com/PCJones/UmlautAda
## Installation ## Installation
Momentan ist Docker dafür nötig, wer kein Docker nutzt muss sich noch etwas gedulden. Eine Unraid-App gibt es auch, einfach nach `umlautadaptarr` suchen. Momentan ist Docker dafür nötig, wer kein Docker nutzt muss sich noch etwas gedulden. Eine Unraid-App gibt es auch, einfach nach `umlautadaptarr` suchen.
[Link zum Docker Image](https://hub.docker.com/r/pcjones/umlautadaptarr) - [Docker](https://hub.docker.com/r/pcjones/umlautadaptarr)
- [Proxmox LXC (unofficial)](https://github.com/elvito/ProxmoxVE/blob/main/ct/umlautadaptarr.sh)
- Unraid: nach `umlautadaptarr` suchen
- [Seedbox/Binary](https://github.com/PCJones/UmlautAdaptarr/blob/master/run_on_seedbox.sh)
Nicht benötigte Umgebungsvariablen, z.B. falls Readarr oder Lidarr nicht genutzt werden, können entfernt werden. Nicht benötigte Umgebungsvariablen, z.B. falls Readarr oder Lidarr nicht genutzt werden, können entfernt werden.
@@ -124,7 +127,9 @@ Sonarr erwartet immer den Englischen Namen, der hier natürlich nicht gegeben is
## Spenden ## Spenden
Über eine Spende freue ich mich natürlich immer :D Über eine Spende freue ich mich natürlich immer :D
PayPal: https://paypal.me/pcjones1
<a href="https://www.buymeacoffee.com/pcjones" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="60px" width="217px" ></a>
<a href="https://coindrop.to/pcjones" target="_blank"><img src="https://coindrop.to/embed-button.png" style="border-radius: 10px; height: 57px !important;width: 229px !important;" alt="Coindrop.to me"></img></a>
Für andere Spendenmöglichkeiten gerne auf Discord oder Telegram melden - danke! Für andere Spendenmöglichkeiten gerne auf Discord oder Telegram melden - danke!

View File

@@ -1,19 +1,30 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Text; using System.Text;
using System.Xml.Linq; using System.Xml.Linq;
using UmlautAdaptarr.Options;
using UmlautAdaptarr.Services; using UmlautAdaptarr.Services;
using UmlautAdaptarr.Utilities; using UmlautAdaptarr.Utilities;
namespace UmlautAdaptarr.Controllers namespace UmlautAdaptarr.Controllers
{ {
public class CapsController(ProxyRequestService proxyRequestService) : ControllerBase public class CapsController(ProxyRequestService proxyRequestService, IOptions<GlobalOptions> options, ILogger<CapsController> logger) : ControllerBase
{ {
private readonly ProxyRequestService _proxyRequestService = proxyRequestService; private readonly ProxyRequestService _proxyRequestService = proxyRequestService;
private readonly GlobalOptions _options = options.Value;
private readonly ILogger<CapsController> _logger = logger;
[HttpGet] [HttpGet]
public async Task<IActionResult> Caps([FromRoute] string options, [FromRoute] string domain, [FromQuery] string? apikey) public async Task<IActionResult> Caps([FromRoute] string apiKey, [FromRoute] string domain, [FromQuery] string? apikey)
{ {
if (!UrlUtilities.IsValidDomain(domain)) if (!string.IsNullOrEmpty(apikey) && !apiKey.Equals(apiKey))
{
_logger.LogWarning("Invalid or missing API key for request.");
return Unauthorized("Unauthorized: Invalid or missing API key.");
}
if (!domain.StartsWith("localhost") && !UrlUtilities.IsValidDomain(domain))
{ {
return NotFound($"{domain} is not a valid URL."); return NotFound($"{domain} is not a valid URL.");
} }

View File

@@ -1,23 +1,31 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Text; using System.Text;
using UmlautAdaptarr.Models; using UmlautAdaptarr.Models;
using UmlautAdaptarr.Options;
using UmlautAdaptarr.Providers;
using UmlautAdaptarr.Services; using UmlautAdaptarr.Services;
using UmlautAdaptarr.Utilities; using UmlautAdaptarr.Utilities;
namespace UmlautAdaptarr.Controllers namespace UmlautAdaptarr.Controllers
{ {
public abstract class SearchControllerBase(ProxyRequestService proxyRequestService, TitleMatchingService titleMatchingService) : ControllerBase public abstract class SearchControllerBase(ProxyRequestService proxyRequestService, TitleMatchingService titleMatchingService, IOptions<GlobalOptions> options, ILogger<SearchControllerBase> logger) : ControllerBase
{ {
// TODO evaluate if this should be set to true by default // TODO evaluate if this should be set to true by default
private readonly bool TODO_FORCE_TEXT_SEARCH_ORIGINAL_TITLE = true; private readonly bool TODO_FORCE_TEXT_SEARCH_ORIGINAL_TITLE = true;
private readonly bool TODO_FORCE_TEXT_SEARCH_GERMAN_TITLE = false; private readonly bool TODO_FORCE_TEXT_SEARCH_GERMAN_TITLE = false;
protected async Task<IActionResult> BaseSearch(string options, protected async Task<IActionResult> BaseSearch(string apiKey,
string domain, string domain,
IDictionary<string, string> queryParameters, IDictionary<string, string> queryParameters,
SearchItem? searchItem = null) SearchItem? searchItem = null)
{ {
try try
{ {
if (!AssureApiKey(apiKey))
{
return Unauthorized("Unauthorized: Invalid or missing API key.");
}
if (!UrlUtilities.IsValidDomain(domain)) if (!UrlUtilities.IsValidDomain(domain))
{ {
return NotFound($"{domain} is not a valid URL."); return NotFound($"{domain} is not a valid URL.");
@@ -110,7 +118,15 @@ namespace UmlautAdaptarr.Controllers
private string ProcessContent(string content, SearchItem? searchItem) private string ProcessContent(string content, SearchItem? searchItem)
{ {
return titleMatchingService.RenameTitlesInContent(content, searchItem); try
{
return titleMatchingService.RenameTitlesInContent(content, searchItem);
}
catch (Exception ex)
{
logger.LogError($"Error at ProcessContent: {ex.Message}{Environment.NewLine}Content:{Environment.NewLine}{content}");
}
return null;
} }
public async Task<AggregatedSearchResult> AggregateSearchResults( public async Task<AggregatedSearchResult> AggregateSearchResults(
@@ -150,29 +166,50 @@ namespace UmlautAdaptarr.Controllers
return aggregatedResult; return aggregatedResult;
} }
internal bool AssureApiKey(string apiKey)
{
if (!string.IsNullOrEmpty(options.Value.ApiKey) && !apiKey.Equals(options.Value.ApiKey))
{
logger.LogWarning("Invalid or missing API key for request.");
return false;
}
return true;
}
} }
public class SearchController(ProxyRequestService proxyRequestService, public class SearchController(ProxyRequestService proxyRequestService,
TitleMatchingService titleMatchingService, TitleMatchingService titleMatchingService,
SearchItemLookupService searchItemLookupService) : SearchControllerBase(proxyRequestService, titleMatchingService) SearchItemLookupService searchItemLookupService,
IOptions<GlobalOptions> options,
ILogger<SearchControllerBase> logger) : SearchControllerBase(proxyRequestService, titleMatchingService, options, logger)
{ {
public readonly string[] LIDARR_CATEGORY_IDS = ["3000", "3010", "3020", "3040", "3050"]; public readonly string[] LIDARR_CATEGORY_IDS = ["3000", "3010", "3020", "3040", "3050"];
public readonly string[] READARR_CATEGORY_IDS = ["3030", "3130", "7000", "7010", "7020", "7030", "7100", "7110", "7120", "7130"]; public readonly string[] READARR_CATEGORY_IDS = ["3030", "3130", "7000", "7010", "7020", "7030", "7100", "7110", "7120", "7130"];
[HttpGet] [HttpGet]
public async Task<IActionResult> MovieSearch([FromRoute] string options, [FromRoute] string domain) public async Task<IActionResult> MovieSearch([FromRoute] string apiKey, [FromRoute] string domain)
{ {
if (!AssureApiKey(apiKey))
{
return Unauthorized("Unauthorized: Invalid or missing API key.");
}
var queryParameters = HttpContext.Request.Query.ToDictionary( var queryParameters = HttpContext.Request.Query.ToDictionary(
q => q.Key, q => q.Key,
q => string.Join(",", q.Value)); q => string.Join(",", q.Value));
return await BaseSearch(options, domain, queryParameters); return await BaseSearch(apiKey, domain, queryParameters);
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> GenericSearch([FromRoute] string options, [FromRoute] string domain) public async Task<IActionResult> GenericSearch([FromRoute] string apiKey, [FromRoute] string domain)
{ {
if (!AssureApiKey(apiKey))
{
return Unauthorized("Unauthorized: Invalid or missing API key.");
}
var queryParameters = HttpContext.Request.Query.ToDictionary( var queryParameters = HttpContext.Request.Query.ToDictionary(
q => q.Key, q => q.Key,
q => string.Join(",", q.Value)); q => string.Join(",", q.Value));
@@ -198,21 +235,31 @@ namespace UmlautAdaptarr.Controllers
} }
} }
return await BaseSearch(options, domain, queryParameters, searchItem); return await BaseSearch(apiKey, domain, queryParameters, searchItem);
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> BookSearch([FromRoute] string options, [FromRoute] string domain) public async Task<IActionResult> BookSearch([FromRoute] string apiKey, [FromRoute] string domain)
{ {
if (!AssureApiKey(apiKey))
{
return Unauthorized("Unauthorized: Invalid or missing API key.");
}
var queryParameters = HttpContext.Request.Query.ToDictionary( var queryParameters = HttpContext.Request.Query.ToDictionary(
q => q.Key, q => q.Key,
q => string.Join(",", q.Value)); q => string.Join(",", q.Value));
return await BaseSearch(options, domain, queryParameters); return await BaseSearch(apiKey, domain, queryParameters);
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> TVSearch([FromRoute] string options, [FromRoute] string domain) public async Task<IActionResult> TVSearch([FromRoute] string apiKey, [FromRoute] string domain)
{ {
if (!AssureApiKey(apiKey))
{
return Unauthorized("Unauthorized: Invalid or missing API key.");
}
var queryParameters = HttpContext.Request.Query.ToDictionary( var queryParameters = HttpContext.Request.Query.ToDictionary(
q => q.Key, q => q.Key,
q => string.Join(",", q.Value)); q => string.Join(",", q.Value));
@@ -229,16 +276,21 @@ namespace UmlautAdaptarr.Controllers
searchItem = await searchItemLookupService.GetOrFetchSearchItemByTitle(mediaType, title); searchItem = await searchItemLookupService.GetOrFetchSearchItemByTitle(mediaType, title);
} }
return await BaseSearch(options, domain, queryParameters, searchItem); return await BaseSearch(apiKey, domain, queryParameters, searchItem);
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> MusicSearch([FromRoute] string options, [FromRoute] string domain) public async Task<IActionResult> MusicSearch([FromRoute] string apiKey, [FromRoute] string domain)
{ {
if (!AssureApiKey(apiKey))
{
return Unauthorized("Unauthorized: Invalid or missing API key.");
}
var queryParameters = HttpContext.Request.Query.ToDictionary( var queryParameters = HttpContext.Request.Query.ToDictionary(
q => q.Key, q => q.Key,
q => string.Join(",", q.Value)); q => string.Join(",", q.Value));
return await BaseSearch(options, domain, queryParameters); return await BaseSearch(apiKey, domain, queryParameters);
} }
} }
} }

View File

@@ -14,5 +14,18 @@
/// The User-Agent string used in HTTP requests. /// The User-Agent string used in HTTP requests.
/// </summary> /// </summary>
public string UserAgent { get; set; } public string UserAgent { get; set; }
/// <summary>
/// The duration in minutes to cache the indexer requests.
/// </summary>
public int IndexerRequestsCacheDurationInMinutes { get; set; } = 12;
/// <summary>
/// API key for requests to the UmlautAdaptarr. Optional.
public string? ApiKey { get; set; } = null;
/// <summary>
/// Proxy port for the internal UmlautAdaptarr proxy.
public int ProxyPort { get; set; } = 5006;
} }
} }

View File

@@ -10,8 +10,11 @@ internal class Program
{ {
private static void Main(string[] args) private static void Main(string[] args)
{ {
Helper.ShowLogo(); MainAsync(args).Wait();
Helper.ShowInformation(); }
private static async Task MainAsync(string[] args)
{
// TODO: // TODO:
// add option to sort by nzb age // add option to sort by nzb age
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -43,9 +46,9 @@ internal class Program
builder.AddTitleLookupService(); builder.AddTitleLookupService();
builder.Services.AddSingleton<SearchItemLookupService>(); builder.Services.AddSingleton<SearchItemLookupService>();
builder.Services.AddSingleton<TitleMatchingService>(); builder.Services.AddSingleton<TitleMatchingService>();
builder.AddSonarrSupport(); await builder.AddSonarrSupport();
builder.AddLidarrSupport(); await builder.AddLidarrSupport();
builder.AddReadarrSupport(); await builder.AddReadarrSupport();
builder.Services.AddSingleton<CacheService>(); builder.Services.AddSingleton<CacheService>();
builder.Services.AddSingleton<ProxyRequestService>(); builder.Services.AddSingleton<ProxyRequestService>();
builder.Services.AddSingleton<ArrApplicationFactory>(); builder.Services.AddSingleton<ArrApplicationFactory>();
@@ -54,37 +57,44 @@ internal class Program
var app = builder.Build(); var app = builder.Build();
Helper.ShowLogo();
if (app.Configuration.GetValue<bool>("IpLeakTest:Enabled"))
{
await Helper.ShowInformation();
}
GlobalStaticLogger.Initialize(app.Services.GetService<ILoggerFactory>()!); GlobalStaticLogger.Initialize(app.Services.GetService<ILoggerFactory>()!);
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllerRoute("caps", app.MapControllerRoute("caps",
"{options}/{*domain}", "{apiKey}/{*domain}",
new { controller = "Caps", action = "Caps" }, new { controller = "Caps", action = "Caps" },
new { t = new TRouteConstraint("caps") }); new { t = new TRouteConstraint("caps") });
app.MapControllerRoute("movie-search", app.MapControllerRoute("movie-search",
"{options}/{*domain}", "{apiKey}/{*domain}",
new { controller = "Search", action = "MovieSearch" }, new { controller = "Search", action = "MovieSearch" },
new { t = new TRouteConstraint("movie") }); new { t = new TRouteConstraint("movie") });
app.MapControllerRoute("tv-search", app.MapControllerRoute("tv-search",
"{options}/{*domain}", "{apiKey}/{*domain}",
new { controller = "Search", action = "TVSearch" }, new { controller = "Search", action = "TVSearch" },
new { t = new TRouteConstraint("tvsearch") }); new { t = new TRouteConstraint("tvsearch") });
app.MapControllerRoute("music-search", app.MapControllerRoute("music-search",
"{options}/{*domain}", "{apiKey}/{*domain}",
new { controller = "Search", action = "MusicSearch" }, new { controller = "Search", action = "MusicSearch" },
new { t = new TRouteConstraint("music") }); new { t = new TRouteConstraint("music") });
app.MapControllerRoute("book-search", app.MapControllerRoute("book-search",
"{options}/{*domain}", "{apiKey}/{*domain}",
new { controller = "Search", action = "BookSearch" }, new { controller = "Search", action = "BookSearch" },
new { t = new TRouteConstraint("book") }); new { t = new TRouteConstraint("book") });
app.MapControllerRoute("generic-search", app.MapControllerRoute("generic-search",
"{options}/{*domain}", "{apiKey}/{*domain}",
new { controller = "Search", action = "GenericSearch" }, new { controller = "Search", action = "GenericSearch" },
new { t = new TRouteConstraint("search") }); new { t = new TRouteConstraint("search") });
app.Run(); app.Run();
@@ -109,4 +119,4 @@ internal class Program
//.Enrich.With(new ApiKeyMaskingEnricher("appsettings.json")) // TODO - Not working currently //.Enrich.With(new ApiKeyMaskingEnricher("appsettings.json")) // TODO - Not working currently
.CreateLogger(); .CreateLogger();
} }
} }

View File

@@ -48,6 +48,21 @@ public class SonarrClient : ArrClientBase
if (shows != null) if (shows != null)
{ {
_logger.LogInformation($"Successfully fetched {shows.Count} items from Sonarr ({InstanceName})."); _logger.LogInformation($"Successfully fetched {shows.Count} items from Sonarr ({InstanceName}).");
// Bulk request (germanTitle, aliases) for all shows
var tvdbIds = new List<string>();
foreach (var show in shows)
{
if ((string)show.tvdbId is not string tvdbId)
{
continue;
}
tvdbIds.Add(tvdbId);
}
var bulkTitleData = await _titleService.FetchGermanTitlesAndAliasesByExternalIdBulkAsync(tvdbIds);
string? germanTitle;
string[]? aliases;
foreach (var show in shows) foreach (var show in shows)
{ {
var tvdbId = (string)show.tvdbId; var tvdbId = (string)show.tvdbId;
@@ -57,8 +72,16 @@ public class SonarrClient : ArrClientBase
continue; continue;
} }
var (germanTitle, aliases) = if (bulkTitleData.TryGetValue(tvdbId, out var titleData))
await _titleService.FetchGermanTitleAndAliasesByExternalIdAsync(_mediaType, tvdbId); {
(germanTitle, aliases) = titleData;
}
else
{
(germanTitle, aliases) =
await _titleService.FetchGermanTitleAndAliasesByExternalIdAsync(_mediaType, tvdbId);
}
var searchItem = new SearchItem var searchItem = new SearchItem
( (
(int)show.id, (int)show.id,

View File

@@ -4,7 +4,7 @@ using UmlautAdaptarr.Providers;
namespace UmlautAdaptarr.Services.Factory namespace UmlautAdaptarr.Services.Factory
{ {
/// <summary> /// <summary>
/// Factory for creating RrApplication instances. /// Factory for creating ArrApplication instances.
/// </summary> /// </summary>
public class ArrApplicationFactory public class ArrApplicationFactory
{ {
@@ -33,26 +33,26 @@ namespace UmlautAdaptarr.Services.Factory
/// <summary> /// <summary>
/// Constructor for the ArrApplicationFactory. /// Constructor for the ArrApplicationFactory.
/// </summary> /// </summary>
/// <param name="rrArrApplications">A dictionary of IArrApplication instances.</param> /// <param name="arrApplications">A dictionary of IArrApplication instances.</param>
/// <param name="logger">Logger Instanz</param> /// <param name="logger">Logger Instanz</param>
public ArrApplicationFactory(IDictionary<string, IArrApplication> rrArrApplications, ILogger<ArrApplicationFactory> logger) public ArrApplicationFactory(IDictionary<string, IArrApplication> arrApplications, ILogger<ArrApplicationFactory> logger)
{ {
_logger = logger; _logger = logger;
try try
{ {
SonarrInstances = rrArrApplications.Values.OfType<SonarrClient>(); SonarrInstances = arrApplications.Values.OfType<SonarrClient>();
LidarrInstances = rrArrApplications.Values.OfType<LidarrClient>(); LidarrInstances = arrApplications.Values.OfType<LidarrClient>();
ReadarrInstances = rrArrApplications.Values.OfType<ReadarrClient>(); ReadarrInstances = arrApplications.Values.OfType<ReadarrClient>();
AllInstances = rrArrApplications; AllInstances = arrApplications;
if (!AllInstances.Values.Any()) if (AllInstances.Values.Count == 0)
{ {
throw new Exception("No RrApplication could be successfully initialized. This could be due to a faulty configuration"); throw new Exception("No ArrApplication could be successfully initialized. This could be due to a faulty configuration");
} }
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError("Error while Register ArrFactory. This might be a Config Problem", e.Message); _logger.LogError("Error while registering ArrFactory. This is most likely a config problem, please check your environment variables.", e.Message);
throw; throw;
} }
} }

View File

@@ -1,6 +1,8 @@
using System.Net; using Microsoft.Extensions.Options;
using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
using UmlautAdaptarr.Options;
namespace UmlautAdaptarr.Services namespace UmlautAdaptarr.Services
{ {
@@ -8,15 +10,16 @@ namespace UmlautAdaptarr.Services
{ {
private TcpListener _listener; private TcpListener _listener;
private readonly ILogger<HttpProxyService> _logger; private readonly ILogger<HttpProxyService> _logger;
private readonly int _proxyPort = 5006; // TODO move to appsettings.json
private readonly IHttpClientFactory _clientFactory; private readonly IHttpClientFactory _clientFactory;
private readonly GlobalOptions _options;
private readonly HashSet<string> _knownHosts = []; private readonly HashSet<string> _knownHosts = [];
private readonly object _hostsLock = new(); private readonly object _hostsLock = new();
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private static readonly string[] newLineSeparator = ["\r\n"]; private static readonly string[] newLineSeparator = ["\r\n"];
public HttpProxyService(ILogger<HttpProxyService> logger, IHttpClientFactory clientFactory, IConfiguration configuration) public HttpProxyService(ILogger<HttpProxyService> logger, IHttpClientFactory clientFactory, IConfiguration configuration, IOptions<GlobalOptions> options)
{ {
_options = options.Value;
_logger = logger; _logger = logger;
_configuration = configuration; _configuration = configuration;
_clientFactory = clientFactory; _clientFactory = clientFactory;
@@ -39,6 +42,24 @@ namespace UmlautAdaptarr.Services
var bytesRead = await clientStream.ReadAsync(buffer); var bytesRead = await clientStream.ReadAsync(buffer);
var requestString = Encoding.ASCII.GetString(buffer, 0, bytesRead); var requestString = Encoding.ASCII.GetString(buffer, 0, bytesRead);
if (!string.IsNullOrEmpty(_options.ApiKey))
{
var headers = ParseHeaders(buffer, bytesRead);
if (!headers.TryGetValue("Proxy-Authorization", out var proxyAuthorizationHeader) ||
!ValidateApiKey(proxyAuthorizationHeader))
{
var isFirstRequest = !headers.ContainsKey("Proxy-Authorization");
if (!isFirstRequest)
{
_logger.LogWarning("Unauthorized access attempt.");
}
await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"Proxy\"\r\n\r\n"));
clientSocket.Close();
return;
}
}
if (requestString.StartsWith("CONNECT")) if (requestString.StartsWith("CONNECT"))
{ {
// Handle HTTPS CONNECT request // Handle HTTPS CONNECT request
@@ -50,6 +71,19 @@ namespace UmlautAdaptarr.Services
await HandleHttp(requestString, clientStream, clientSocket, buffer, bytesRead); await HandleHttp(requestString, clientStream, clientSocket, buffer, bytesRead);
} }
} }
private bool ValidateApiKey(string proxyAuthorizationHeader)
{
// Expect the header to be in the format: "Basic <base64encodedApiKey>"
if (proxyAuthorizationHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
{
var encodedKey = proxyAuthorizationHeader["Basic ".Length..].Trim();
var decodedKey = Encoding.ASCII.GetString(Convert.FromBase64String(encodedKey));
var password = decodedKey.Split(':')[^1];
return password == _options.ApiKey;
}
return false;
}
private async Task HandleHttpsConnect(string requestString, NetworkStream clientStream, Socket clientSocket) private async Task HandleHttpsConnect(string requestString, NetworkStream clientStream, Socket clientSocket)
{ {
@@ -96,7 +130,9 @@ namespace UmlautAdaptarr.Services
var url = _configuration["Kestrel:Endpoints:Http:Url"]; var url = _configuration["Kestrel:Endpoints:Http:Url"];
var port = new Uri(url).Port; var port = new Uri(url).Port;
var modifiedUri = $"http://localhost:{port}/_/{uri.Host}{uri.PathAndQuery}"; var apiKey = string.IsNullOrEmpty(_options.ApiKey) ? "_" : _options.ApiKey;
var modifiedUri = $"http://localhost:{port}/{apiKey}/{uri.Host}{uri.PathAndQuery}";
using var client = _clientFactory.CreateClient(); using var client = _clientFactory.CreateClient();
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, modifiedUri); var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, modifiedUri);
httpRequestMessage.Headers.Add("User-Agent", userAgent); httpRequestMessage.Headers.Add("User-Agent", userAgent);
@@ -168,7 +204,7 @@ namespace UmlautAdaptarr.Services
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
_listener = new TcpListener(IPAddress.Any, _proxyPort); _listener = new TcpListener(IPAddress.Any, _options.ProxyPort);
_listener.Start(); _listener.Start();
Task.Run(() => HandleRequests(cancellationToken), cancellationToken); Task.Run(() => HandleRequests(cancellationToken), cancellationToken);
return Task.CompletedTask; return Task.CompletedTask;

View File

@@ -81,7 +81,7 @@ namespace UmlautAdaptarr.Services
if (responseMessage.IsSuccessStatusCode) if (responseMessage.IsSuccessStatusCode)
{ {
_cache.Set(targetUri, responseMessage, TimeSpan.FromMinutes(12)); _cache.Set(targetUri, responseMessage, TimeSpan.FromMinutes(_options.IndexerRequestsCacheDurationInMinutes));
} }
return responseMessage; return responseMessage;

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System.Text;
using UmlautAdaptarr.Options; using UmlautAdaptarr.Options;
using UmlautAdaptarr.Utilities; using UmlautAdaptarr.Utilities;
@@ -22,7 +23,7 @@ namespace UmlautAdaptarr.Services
lastRequestTime = DateTime.Now; lastRequestTime = DateTime.Now;
} }
// TODO add cache, TODO add bulk request // TODO add caching
public async Task<(string? germanTitle, string[]? aliases)> FetchGermanTitleAndAliasesByExternalIdAsync(string mediaType, string externalId) public async Task<(string? germanTitle, string[]? aliases)> FetchGermanTitleAndAliasesByExternalIdAsync(string mediaType, string externalId)
{ {
try try
@@ -68,6 +69,68 @@ namespace UmlautAdaptarr.Services
return (null, null); return (null, null);
} }
public async Task<Dictionary<string, (string? germanTitle, string[]? aliases)>> FetchGermanTitlesAndAliasesByExternalIdBulkAsync(IEnumerable<string> tvdbIds)
{
try
{
await EnsureMinimumDelayAsync();
var httpClient = clientFactory.CreateClient();
var bulkApiUrl = $"{Options.UmlautAdaptarrApiHost}/tvshow_german.php?bulk=true";
logger.LogInformation($"TitleApiService POST {UrlUtilities.RedactApiKey(bulkApiUrl)}");
// Prepare POST request payload
var payload = new { tvdbIds = tvdbIds.ToArray() };
var jsonPayload = JsonConvert.SerializeObject(payload);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
// Send POST request
var response = await httpClient.PostAsync(bulkApiUrl, content);
if (!response.IsSuccessStatusCode)
{
logger.LogError($"Failed to fetch German titles via bulk API. Status Code: {response.StatusCode}");
return [];
}
var responseContent = await response.Content.ReadAsStringAsync();
var bulkApiResponseData = JsonConvert.DeserializeObject<dynamic>(responseContent);
if (bulkApiResponseData == null || bulkApiResponseData.status != "success")
{
logger.LogError($"Parsing UmlautAdaptarr Bulk API response resulted in null or an error status.");
return [];
}
// Process response data
var results = new Dictionary<string, (string? germanTitle, string[]? aliases)>();
foreach (var entry in bulkApiResponseData.data)
{
string tvdbId = entry.tvdbId;
string? germanTitle = entry.germanTitle;
string[]? aliases = null;
if (entry.aliases != null)
{
JArray aliasesArray = JArray.FromObject(entry.aliases);
aliases = aliasesArray.Children<JObject>()
.Select(alias => alias["name"].ToString())
.ToArray();
}
results[tvdbId] = (germanTitle, aliases);
}
logger.LogInformation($"Successfully fetched German titles for {results.Count} TVDB IDs via bulk API.");
return results;
}
catch (Exception ex)
{
logger.LogError($"Error fetching German titles in bulk: {ex.Message}");
return new Dictionary<string, (string? germanTitle, string[]? aliases)>();
}
}
public async Task<(string? germanTitle, string? externalId, string[]? aliases)> FetchGermanTitleAndExternalIdAndAliasesByTitle(string mediaType, string title) public async Task<(string? germanTitle, string? externalId, string[]? aliases)> FetchGermanTitleAndExternalIdAndAliasesByTitle(string mediaType, string title)
{ {
try try

View File

@@ -9,13 +9,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" /> <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0-preview1" />
<PackageReference Include="IL.FluentValidation.Extensions.Options" Version="11.0.2" /> <PackageReference Include="IL.FluentValidation.Extensions.Options" Version="11.0.2" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" /> <PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2" /> <PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.3" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -11,10 +11,10 @@ public static class Helper
"\r\n _ _ _ _ ___ _ _ \r\n| | | | | | | | / _ \\ | | | | \r\n| | | |_ __ ___ | | __ _ _ _| |_/ /_\\ \\ __| | __ _ _ __ | |_ __ _ _ __ _ __ \r\n| | | | '_ ` _ \\| |/ _` | | | | __| _ |/ _` |/ _` | '_ \\| __/ _` | '__| '__|\r\n| |_| | | | | | | | (_| | |_| | |_| | | | (_| | (_| | |_) | || (_| | | | | \r\n \\___/|_| |_| |_|_|\\__,_|\\__,_|\\__\\_| |_/\\__,_|\\__,_| .__/ \\__\\__,_|_| |_| \r\n | | \r\n |_| \r\n"); "\r\n _ _ _ _ ___ _ _ \r\n| | | | | | | | / _ \\ | | | | \r\n| | | |_ __ ___ | | __ _ _ _| |_/ /_\\ \\ __| | __ _ _ __ | |_ __ _ _ __ _ __ \r\n| | | | '_ ` _ \\| |/ _` | | | | __| _ |/ _` |/ _` | '_ \\| __/ _` | '__| '__|\r\n| |_| | | | | | | | (_| | |_| | |_| | | | (_| | (_| | |_) | || (_| | | | | \r\n \\___/|_| |_| |_|_|\\__,_|\\__,_|\\__\\_| |_/\\__,_|\\__,_| .__/ \\__\\__,_|_| |_| \r\n | | \r\n |_| \r\n");
} }
public static void ShowInformation() public static async Task ShowInformation()
{ {
Console.WriteLine("--------------------------[IP Leak Test]-----------------------------"); Console.WriteLine("--------------------------[IP Leak Test]-----------------------------");
var ipInfo = GetPublicIpAddressInfoAsync().GetAwaiter().GetResult(); var ipInfo = await GetPublicIpAddressInfoAsync();
if (ipInfo != null) if (ipInfo != null)
{ {

View File

@@ -29,7 +29,7 @@ public static class ServicesExtensions
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param> /// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
/// <param name="sectionName">The name of the configuration section containing service options.</param> /// <param name="sectionName">The name of the configuration section containing service options.</param>
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns> /// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
private static WebApplicationBuilder AddServicesWithOptions<TOptions, TService, TInterface>( private static async Task<WebApplicationBuilder> AddServicesWithOptions<TOptions, TService, TInterface>(
this WebApplicationBuilder builder, string sectionName) this WebApplicationBuilder builder, string sectionName)
where TOptions : class, new() where TOptions : class, new()
where TService : class, TInterface where TService : class, TInterface
@@ -57,9 +57,9 @@ public static class ServicesExtensions
foreach (var option in optionsArray) foreach (var option in optionsArray)
{ {
GlobalInstanceOptionsValidator validator = new GlobalInstanceOptionsValidator(); GlobalInstanceOptionsValidator validator = new();
var results = validator.Validate(option as GlobalInstanceOptions); var results = await validator.ValidateAsync(option as GlobalInstanceOptions);
if (!results.IsValid) if (!results.IsValid)
{ {
@@ -68,7 +68,7 @@ public static class ServicesExtensions
Console.WriteLine(($"Property {failure.PropertyName } failed validation. Error was: {failure.ErrorMessage}")); Console.WriteLine(($"Property {failure.PropertyName } failed validation. Error was: {failure.ErrorMessage}"));
} }
throw new Exception("Please fix first you config and then Start UmlautAdaptarr again"); throw new Exception("Please fix cour environment variables and then Start UmlautAdaptarr again");
} }
var instanceState = (bool)(typeof(TOptions).GetProperty("Enabled")?.GetValue(option, null) ?? false); var instanceState = (bool)(typeof(TOptions).GetProperty("Enabled")?.GetValue(option, null) ?? false);
@@ -143,7 +143,7 @@ public static class ServicesExtensions
/// </summary> /// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param> /// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns> /// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
public static WebApplicationBuilder AddSonarrSupport(this WebApplicationBuilder builder) public static Task<WebApplicationBuilder> AddSonarrSupport(this WebApplicationBuilder builder)
{ {
// builder.Serviceses.AddSingleton<IOptionsMonitoSonarrInstanceOptionsns>, OptionsMonitoSonarrInstanceOptionsns>>(); // builder.Serviceses.AddSingleton<IOptionsMonitoSonarrInstanceOptionsns>, OptionsMonitoSonarrInstanceOptionsns>>();
return builder.AddServicesWithOptions<SonarrInstanceOptions, SonarrClient, IArrApplication>("Sonarr"); return builder.AddServicesWithOptions<SonarrInstanceOptions, SonarrClient, IArrApplication>("Sonarr");
@@ -154,7 +154,7 @@ public static class ServicesExtensions
/// </summary> /// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param> /// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns> /// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
public static WebApplicationBuilder AddLidarrSupport(this WebApplicationBuilder builder) public static Task<WebApplicationBuilder> AddLidarrSupport(this WebApplicationBuilder builder)
{ {
return builder.AddServicesWithOptions<LidarrInstanceOptions, LidarrClient, IArrApplication>("Lidarr"); return builder.AddServicesWithOptions<LidarrInstanceOptions, LidarrClient, IArrApplication>("Lidarr");
} }
@@ -164,7 +164,7 @@ public static class ServicesExtensions
/// </summary> /// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param> /// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns> /// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
public static WebApplicationBuilder AddReadarrSupport(this WebApplicationBuilder builder) public static Task<WebApplicationBuilder> AddReadarrSupport(this WebApplicationBuilder builder)
{ {
return builder.AddServicesWithOptions<ReadarrInstanceOptions, ReadarrClient, IArrApplication>("Readarr"); return builder.AddServicesWithOptions<ReadarrInstanceOptions, ReadarrClient, IArrApplication>("Readarr");
} }

View File

@@ -5,7 +5,8 @@ namespace UmlautAdaptarr.Utilities
{ {
public partial class UrlUtilities public partial class UrlUtilities
{ {
[GeneratedRegex(@"^(?!http:\/\/)([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+.*)$")] [GeneratedRegex(@"^(?!http:\/\/)([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+(:\d+)?(/.*)?)$")]
private static partial Regex UrlMatchingRegex(); private static partial Regex UrlMatchingRegex();
public static bool IsValidDomain(string domain) public static bool IsValidDomain(string domain)
{ {

View File

@@ -1,11 +1,15 @@
using System.Net; using FluentValidation;
using FluentValidation;
using UmlautAdaptarr.Options.ArrOptions.InstanceOptions; using UmlautAdaptarr.Options.ArrOptions.InstanceOptions;
namespace UmlautAdaptarr.Validator; namespace UmlautAdaptarr.Validator;
public class GlobalInstanceOptionsValidator : AbstractValidator<GlobalInstanceOptions> public class GlobalInstanceOptionsValidator : AbstractValidator<GlobalInstanceOptions>
{ {
private readonly static HttpClient httpClient = new()
{
Timeout = TimeSpan.FromSeconds(3)
};
public GlobalInstanceOptionsValidator() public GlobalInstanceOptionsValidator()
{ {
RuleFor(x => x.Enabled).NotNull(); RuleFor(x => x.Enabled).NotNull();
@@ -14,12 +18,14 @@ public class GlobalInstanceOptionsValidator : AbstractValidator<GlobalInstanceOp
{ {
RuleFor(x => x.Host) RuleFor(x => x.Host)
.NotEmpty().WithMessage("Host is required when Enabled is true.") .NotEmpty().WithMessage("Host is required when Enabled is true.")
.Must(BeAValidUrl).WithMessage("Host/Url must start with http:// or https:// and be a valid address.") .Must(BeAValidUrl).WithMessage("Host/Url must start with http:// or https:// and be a valid address.");
.Must(BeReachable)
.WithMessage("Host/Url is not reachable. Please check your Host or your UmlautAdaptrr Settings");
RuleFor(x => x.ApiKey) RuleFor(x => x.ApiKey)
.NotEmpty().WithMessage("ApiKey is required when Enabled is true."); .NotEmpty().WithMessage("ApiKey is required when Enabled is true.");
RuleFor(x => x)
.MustAsync(BeReachable)
.WithMessage("Host/Url is not reachable. Please check your Host or your UmlautAdaptrr Settings");
}); });
} }
@@ -29,54 +35,37 @@ public class GlobalInstanceOptionsValidator : AbstractValidator<GlobalInstanceOp
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
} }
private static bool BeReachable(string url) private static async Task<bool> BeReachable(GlobalInstanceOptions opts, CancellationToken cancellationToken)
{ {
var endTime = DateTime.Now.AddMinutes(3); var endTime = DateTime.Now.AddMinutes(3);
var reachable = false; var reachable = false;
var url = $"{opts.Host}/api?apikey={opts.ApiKey}";
while (DateTime.Now < endTime) while (DateTime.Now < endTime)
{ {
try try
{ {
// TODO use HttpClient here using var response = await httpClient.GetAsync(url, cancellationToken);
var request = (HttpWebRequest)WebRequest.Create(url); if (response.IsSuccessStatusCode)
request.AllowAutoRedirect = false;
request.Timeout = 3000;
using var response = (HttpWebResponse)request.GetResponse();
reachable = response.StatusCode == HttpStatusCode.OK;
if (reachable)
{ {
reachable = true;
break; break;
} }
// If status is 301/302 (Found), follow the redirect manually else
else if (response.StatusCode == HttpStatusCode.MovedPermanently || response.StatusCode == HttpStatusCode.Found)
{ {
var redirectUrl = response.Headers["Location"]; // Get the redirect URL Console.WriteLine($"Reachable check got unexpected status code {response.StatusCode}.");
if (!string.IsNullOrEmpty(redirectUrl))
{
// Create a new request for the redirected URL
var redirectRequest = (HttpWebRequest)WebRequest.Create(redirectUrl);
redirectRequest.Timeout = 3000;
using var redirectResponse = (HttpWebResponse)redirectRequest.GetResponse();
reachable = redirectResponse.StatusCode == HttpStatusCode.OK;
if (reachable)
{
break;
}
}
} }
} }
catch catch (Exception ex)
{ {
Console.WriteLine(ex.Message);
} }
// Wait for 15 seconds for next try // Wait for 15 seconds for next try
Console.WriteLine($"The URL \"{url}\" is not reachable. Next attempt in 15 seconds..."); Console.WriteLine($"The URL \"{opts.Host}/api?apikey=[REDACTED]\" is not reachable. Next attempt in 15 seconds...");
Thread.Sleep(15000); Thread.Sleep(15000);
} }
return reachable; return reachable;
} }
} }

View File

@@ -20,7 +20,10 @@
// Settings__UmlautAdaptarrApiHost=https://umlautadaptarr.pcjones.de/api/v1 // Settings__UmlautAdaptarrApiHost=https://umlautadaptarr.pcjones.de/api/v1
"Settings": { "Settings": {
"UserAgent": "UmlautAdaptarr/1.0", "UserAgent": "UmlautAdaptarr/1.0",
"UmlautAdaptarrApiHost": "https://umlautadaptarr.pcjones.de/api/v1" "UmlautAdaptarrApiHost": "https://umlautadaptarr.pcjones.de/api/v1",
"IndexerRequestsCacheDurationInMinutes": 12,
"ApiKey": null,
"ProxyPort": 5006
}, },
"Sonarr": [ "Sonarr": [
{ {
@@ -64,5 +67,10 @@
"Enabled": false, "Enabled": false,
"Host": "your_readarr_host_url", "Host": "your_readarr_host_url",
"ApiKey": "your_readarr_api_key" "ApiKey": "your_readarr_api_key"
},
"IpLeakTest": {
// Docker Environment Variables:
// - IpLeakTest__Enabled: false (set to true to enable)
"Enabled": false
} }
} }

View File

@@ -32,3 +32,11 @@ services:
#- SONARR__1__ENABLED=false #- SONARR__1__ENABLED=false
#- SONARR__1__HOST=http://localhost:8989 #- SONARR__1__HOST=http://localhost:8989
#- SONARR__1__APIKEY=APIKEY #- SONARR__1__APIKEY=APIKEY
### Advanced options (with default values))
#- IpLeakTest__Enabled=false
#- SETTINGS__IndexerRequestsCacheDurationInMinutes=12 # How long to cache indexer requests for. Default is 12 minutes.
#- SETTINGS__ApiKey= # API key for requests to the UmlautAdaptarr. Optional, probably only needed for seedboxes.
#- SETTINGS__ProxyPort=5006 # Proxy port for the internal UmlautAdaptarr proxy used for Prowlarr.
#- Kestrel__Endpoints__Http__Url=http://[::]:5005 # HTTP port for the UmlautAdaptarr

52
run_on_seedbox.sh Normal file
View File

@@ -0,0 +1,52 @@
#!/bin/bash
# Download linux binary from https://github.com/PCJones/UmlautAdaptarr/releases
# script by schumi4 - THX!
#seedbox fix
export DOTNET_GCHeapHardLimit=20000000
# Basic Configuration
export TZ=Europe/Berlin
# Sonarr Configuration
export SONARR__ENABLED=true
export SONARR__HOST=https://name.server.usbx.me/sonarr/
export SONARR__APIKEY=APIKEY
# Radarr Configuration
export RADARR__ENABLED=false
export RADARR__HOST=http://localhost:7878
export RADARR__APIKEY=APIKEY
# Readarr Configuration
export READARR__ENABLED=false
export READARR__HOST=http://localhost:8787
export READARR__APIKEY=APIKEY
# Lidarr Configuration
export LIDARR__ENABLED=false
export LIDARR__HOST=http://localhost:8686
export LIDARR__APIKEY=APIKEY
# Multiple Sonarr Instances (commented out by default)
#export SONARR__0__NAME="NAME 1"
#export SONARR__0__ENABLED=false
#export SONARR__0__HOST=http://localhost:8989
#export SONARR__0__APIKEY=APIKEY
#export SONARR__1__NAME="NAME 2"
#export SONARR__1__ENABLED=false
#export SONARR__1__HOST=http://localhost:8989
#export SONARR__1__APIKEY=APIKEY
# Advanced Options
#export IpLeakTest__Enabled=false
#export SETTINGS__IndexerRequestsCacheDurationInMinutes=12
export SETTINGS__ApiKey="apikey" # Change to something unique! Then in Prowlarr, in the proxy settings set any username and use this ApiKey as password.
export SETTINGS__ProxyPort=1234 # Port for Proxy
export Kestrel__Endpoints__Http__Url="http://[::]:1235" # Port for UmlautAdaptarr API
chmod +x ./publish/UmlautAdaptarr
./publish/UmlautAdaptarr