38 Commits

Author SHA1 Message Date
pcjones
530cbed2d3 Update docker compose 2024-04-15 04:25:21 +02:00
pcjones
94e62cf4dd Forward user-agent when using http proxy 2024-04-15 03:42:20 +02:00
pcjones
c4069e0732 Change warning text 2024-04-15 03:21:51 +02:00
pcjones
6f743eca01 Revert "Add TODO"
This reverts commit 45bc7baa4a.
2024-04-15 03:20:36 +02:00
pcjones
45bc7baa4a Add TODO 2024-04-15 03:17:14 +02:00
pcjones
43717d5fc4 HttpProxyService: Forward https, modify http 2024-04-15 03:05:09 +02:00
pcjones
08e13db32d Revert "Temporarily remove API key redaction"
This reverts commit f3684d24d3.
2024-04-15 00:59:56 +02:00
pcjones
f3684d24d3 Temporarily remove API key redaction 2024-04-15 00:56:13 +02:00
pcjones
cee3c12daa Add HttpProxyService 2024-04-15 00:48:27 +02:00
pcjones
e888a10366 Add timestamp to log 2024-04-15 00:48:15 +02:00
pcjones
660c245069 Remove 5005 port exposing from docker compose 2024-04-14 23:32:21 +02:00
pcjones
49193ef12f Rename ProxyService to ProxyRequestService 2024-04-14 23:22:40 +02:00
pcjones
881f3b7281 Add IPv6 support 2024-04-14 23:15:03 +02:00
pcjones
d098d1fd10 Change cache time from 5 to 12 minutes 2024-04-14 22:49:50 +02:00
pcjones
6cf87620c3 Add TODO 2024-04-14 22:44:51 +02:00
pcjones
389d685e95 Code cleanup 2024-04-14 22:44:07 +02:00
pcjones
12d9217964 Add todo 2024-04-14 22:29:10 +02:00
pcjones
5cd90b7b20 Remove secrets example 2024-04-14 22:28:38 +02:00
Jonas F
93990dbf52 Merge pull request #16 from PCJones/master
Merge master in develop (readme)
2024-04-14 22:22:26 +02:00
Jonas F
49565be191 Merge pull request #15 from xpsony/Proxy_IOption
Add Proxy Support, Add IOptions Pattern, Add Extensions Method
2024-04-14 22:21:23 +02:00
Felix Glang
24d5cb83a4 Add Proxy Support, Add IOptions Pattern, Add Extensions Method
Currently Changes

Http / Https proxy support has been added , To disguise the Ip address or if a proxy service is required
IOptions pattern has been implemented. Better options handling
Extensions methods have been implemented to make Program.cs smaller
Added a global logger for static and extension methods
appsettings.json now contains "default" data for the applications and proxy settings. The Docker variables are also specified above it. This also fixes the bug that you have to set all variables, although you only want to use Sonarr, for example
2024-04-14 16:43:09 +02:00
pcjones
f02547c0e3 Merge branch 'master' of https://github.com/PCJones/UmlautAdaptarr 2024-03-15 18:25:08 +01:00
pcjones
61e93b5b24 Enhance searchitem matching 2024-03-15 18:24:39 +01:00
pcjones
f88daf4955 Lower minimum delay between requests to 1 second 2024-03-08 10:00:29 +01:00
Jonas F
93c667422f Update README.md 2024-03-06 20:03:16 +01:00
Jonas F
e1978d869c Merge pull request #12 from PCJones/develop
v0.4
2024-03-06 20:00:05 +01:00
pcjones
cfdfa89009 Merge branch 'develop' 2024-03-06 19:52:19 +01:00
pcjones
9bee42d7dd Detect media Type by category ID 2024-03-06 19:52:07 +01:00
pcjones
797ff2b97e Fix searchItem title not being logged 2024-03-01 12:53:47 +01:00
pcjones
a67d5c2d1e Merge branch 'develop' 2024-02-29 17:20:23 +01:00
pcjones
d1d05f8264 Fix url ending with / not being recognized as valid 2024-02-29 17:17:55 +01:00
Jonas F
939b902be3 Update README.md 2024-02-26 21:33:45 +01:00
Jonas F
f56d071642 Update README.md 2024-02-26 21:26:15 +01:00
Jonas F
7513e7d227 Update README.md 2024-02-26 21:23:26 +01:00
Jonas F
7a791dab23 Update README.md 2024-02-26 21:23:00 +01:00
Jonas F
402a4deba3 Update README.md 2024-02-26 21:21:05 +01:00
Jonas F
d31508fef3 Update README.md 2024-02-26 21:20:33 +01:00
pcjones
1c329e886d Update README 2024-02-24 16:33:50 +01:00
31 changed files with 648 additions and 271 deletions

View File

@@ -5,7 +5,7 @@
## Erste Testversion
Wer möchte kann den UmlautAdaptarr jetzt gerne testen! Über Feedback würde ich mich sehr freuen!
Es sollte mit allen *arrs funktionieren, hat aber nur bei Sonarr und Lidarr schon Auswirkungen (abgesehen vom Caching).
Es sollte mit allen *arrs funktionieren, hat aber nur bei Sonarr, Readarr und Lidarr schon Auswirkungen (abgesehen vom Caching).
Momentan ist docker dafür nötig, wer kein Docker nutzt muss sich noch etwas gedulden.
@@ -30,7 +30,7 @@ UmlautAdaptarr löst mehrere Probleme:
# Wie macht UmlautAdaptarr das?
UmlautAdaptarr tut so, als wäre es ein Indexer. In Wahrheit schaltet sich UmlautAdaptarr aber nur zwischen die *arrs und den echten Indexer und kann somit die Suchen sowie die Ergebnisse abfangen und bearbeiten.
Am Ende werden die gefundenen Releases immer so umbenannt, das die Arrs sie einwandfrei erkennen.
Am Ende werden die gefundenen Releases immer so umbenannt, dass die Arrs sie einwandfrei erkennen.
Einige Beispiele findet ihr unter Features.
@@ -46,6 +46,8 @@ Einige Beispiele findet ihr unter Features.
| Releases mit TVDB-Alias Titel werden erkannt | ✓ |
| Korrekte Suche und Erkennung von Titel mit Umlauten | ✓ |
| Anfragen-Caching für 5 Minuten zur Reduzierung der API-Zugriffe | ✓ |
| Usenet (newznab) Support |✓|
| Torrent (torznab) Support |✓|
| Radarr Support | Geplant |
| Prowlarr Unterstützung für "DE" SceneNZBs Kategorien | Geplant |
| Unterstützung weiterer Sprachen neben Deutsch | Geplant |

View File

@@ -6,9 +6,9 @@ using UmlautAdaptarr.Utilities;
namespace UmlautAdaptarr.Controllers
{
public class CapsController(ProxyService proxyService) : ControllerBase
public class CapsController(ProxyRequestService proxyRequestService) : ControllerBase
{
private readonly ProxyService _proxyService = proxyService;
private readonly ProxyRequestService _proxyRequestService = proxyRequestService;
[HttpGet]
public async Task<IActionResult> Caps([FromRoute] string options, [FromRoute] string domain, [FromQuery] string? apikey)
@@ -20,7 +20,7 @@ namespace UmlautAdaptarr.Controllers
var requestUrl = UrlUtilities.BuildUrl(domain, "caps", apikey);
var responseMessage = await _proxyService.ProxyRequestAsync(HttpContext, requestUrl);
var responseMessage = await _proxyRequestService.ProxyRequestAsync(HttpContext, requestUrl);
var content = await responseMessage.Content.ReadAsStringAsync();
var encoding = responseMessage.Content.Headers.ContentType?.CharSet != null ?

View File

@@ -6,7 +6,7 @@ using UmlautAdaptarr.Utilities;
namespace UmlautAdaptarr.Controllers
{
public abstract class SearchControllerBase(ProxyService proxyService, TitleMatchingService titleMatchingService) : ControllerBase
public abstract class SearchControllerBase(ProxyRequestService proxyRequestService, TitleMatchingService titleMatchingService) : ControllerBase
{
// TODO evaluate if this should be set to true by default
private readonly bool TODO_FORCE_TEXT_SEARCH_ORIGINAL_TITLE = true;
@@ -96,7 +96,7 @@ namespace UmlautAdaptarr.Controllers
private async Task<IActionResult> PerformSingleSearchRequest(string domain, IDictionary<string, string> queryParameters)
{
var requestUrl = UrlUtilities.BuildUrl(domain, queryParameters);
var responseMessage = await proxyService.ProxyRequestAsync(HttpContext, requestUrl);
var responseMessage = await proxyRequestService.ProxyRequestAsync(HttpContext, requestUrl);
var content = await responseMessage.Content.ReadAsStringAsync();
var encoding = responseMessage.Content.Headers.ContentType?.CharSet != null ?
@@ -130,7 +130,7 @@ namespace UmlautAdaptarr.Controllers
{
queryParameters["q"] = titleVariation; // Replace the "q" parameter for each variation
var requestUrl = UrlUtilities.BuildUrl(domain, queryParameters);
var responseMessage = await proxyService.ProxyRequestAsync(HttpContext, requestUrl);
var responseMessage = await proxyRequestService.ProxyRequestAsync(HttpContext, requestUrl);
var content = await responseMessage.Content.ReadAsStringAsync();
// Only update encoding from the first response
@@ -152,9 +152,9 @@ namespace UmlautAdaptarr.Controllers
}
}
public class SearchController(ProxyService proxyService,
public class SearchController(ProxyRequestService proxyRequestService,
TitleMatchingService titleMatchingService,
SearchItemLookupService searchItemLookupService) : SearchControllerBase(proxyService, titleMatchingService)
SearchItemLookupService searchItemLookupService) : SearchControllerBase(proxyRequestService, titleMatchingService)
{
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"];

View File

@@ -0,0 +1,23 @@
namespace UmlautAdaptarr.Options.ArrOptions
{
/// <summary>
/// Base Options for ARR applications
/// </summary>
public class ArrApplicationBaseOptions
{
/// <summary>
/// Indicates whether the Arr application is enabled.
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// The host of the ARR application.
/// </summary>
public string Host { get; set; }
/// <summary>
/// The API key of the ARR application.
/// </summary>
public string ApiKey { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
namespace UmlautAdaptarr.Options.ArrOptions
{
/// <summary>
/// Lidarr Options
/// </summary>
public class LidarrInstanceOptions : ArrApplicationBaseOptions
{
}
}

View File

@@ -0,0 +1,9 @@
namespace UmlautAdaptarr.Options.ArrOptions
{
/// <summary>
/// Readarr Options
/// </summary>
public class ReadarrInstanceOptions : ArrApplicationBaseOptions
{
}
}

View File

@@ -0,0 +1,9 @@
namespace UmlautAdaptarr.Options.ArrOptions
{
/// <summary>
/// Sonarr Options
/// </summary>
public class SonarrInstanceOptions : ArrApplicationBaseOptions
{
}
}

View File

@@ -0,0 +1,18 @@
namespace UmlautAdaptarr.Options
{
/// <summary>
/// Global options for the UmlautAdaptarr application.
/// </summary>
public class GlobalOptions
{
/// <summary>
/// The host of the UmlautAdaptarr API.
/// </summary>
public string UmlautAdaptarrApiHost { get; set; }
/// <summary>
/// The User-Agent string used in HTTP requests.
/// </summary>
public string UserAgent { get; set; }
}
}

View File

@@ -0,0 +1,27 @@
namespace UmlautAdaptarr.Options;
/// <summary>
/// Represents options for proxy configuration.
/// </summary>
public class Proxy
{
/// <summary>
/// Gets or sets a value indicating whether to use a proxy.
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Gets or sets the address of the proxy.
/// </summary>
public string? Address { get; set; }
/// <summary>
/// Gets or sets the username for proxy authentication.
/// </summary>
public string? Username { get; set; }
/// <summary>
/// Gets or sets the password for proxy authentication.
/// </summary>
public string? Password { get; set; }
}

View File

@@ -0,0 +1,32 @@
namespace UmlautAdaptarr.Options;
/// <summary>
/// Represents options for proxy configuration.
/// </summary>
public class ProxyOptions
{
/// <summary>
/// Gets or sets a value indicating whether to use a proxy.
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Gets or sets the address of the proxy.
/// </summary>
public string? Address { get; set; }
/// <summary>
/// Gets or sets the username for proxy authentication.
/// </summary>
public string? Username { get; set; }
/// <summary>
/// Gets or sets the password for proxy authentication.
/// </summary>
public string? Password { get; set; }
/// <summary>
/// Bypass Local Ip Addresses , Proxy will ignore local Ip Addresses
/// </summary>
public bool BypassOnLocal { get; set; }
}

View File

@@ -1,8 +1,8 @@
using Microsoft.Extensions.Configuration;
using System.Net;
using UmlautAdaptarr.Providers;
using UmlautAdaptarr.Options;
using UmlautAdaptarr.Routing;
using UmlautAdaptarr.Services;
using UmlautAdaptarr.Utilities;
internal class Program
{
@@ -24,11 +24,14 @@ internal class Program
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli
};
var proxyOptions = configuration.GetSection("Proxy").Get<ProxyOptions>();
handler.ConfigureProxy(proxyOptions);
return handler;
});
builder.Services.AddMemoryCache(options =>
{
// TODO cache size limit? option?
//options.SizeLimit = 20000;
});
@@ -46,19 +49,20 @@ internal class Program
builder.Services.AddControllers();
builder.Services.AddHostedService<ArrSyncBackgroundService>();
builder.Services.AddSingleton<TitleApiService>();
builder.AddTitleLookupService();
builder.Services.AddSingleton<SearchItemLookupService>();
builder.Services.AddSingleton<TitleMatchingService>();
builder.Services.AddSingleton<SonarrClient>();
builder.Services.AddSingleton<LidarrClient>();
builder.Services.AddSingleton<ReadarrClient>();
builder.AddSonarrSupport();
builder.AddLidarrSupport();
builder.AddReadarrSupport();
builder.Services.AddSingleton<CacheService>();
builder.Services.AddSingleton<ProxyService>();
builder.Services.AddSingleton<ProxyRequestService>();
builder.Services.AddSingleton<IHostedService, HttpProxyService>();
var app = builder.Build();
GlobalStaticLogger.Initialize(app.Services.GetService<ILoggerFactory>()!);
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllerRoute(name: "caps",

View File

@@ -0,0 +1,17 @@
namespace UmlautAdaptarr.Providers
{
public static class ArrClientFactory
{
// TODO, still uses old IConfiguration
// TODO not used yet
public static IEnumerable<TClient> CreateClients<TClient>(
Func<string, TClient> constructor, IConfiguration configuration, string configKey) where TClient : ArrClientBase
{
var hosts = configuration.GetValue<string>(configKey)?.Split(',') ?? throw new ArgumentException($"{configKey} environment variable must be set if the app is enabled");
foreach (var host in hosts)
{
yield return constructor(host.Trim());
}
}
}
}

View File

@@ -1,7 +1,9 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UmlautAdaptarr.Models;
using UmlautAdaptarr.Options.ArrOptions;
using UmlautAdaptarr.Services;
using UmlautAdaptarr.Utilities;
@@ -9,13 +11,11 @@ namespace UmlautAdaptarr.Providers
{
public class LidarrClient(
IHttpClientFactory clientFactory,
IConfiguration configuration,
CacheService cacheService,
IMemoryCache cache,
ILogger<LidarrClient> logger) : ArrClientBase()
ILogger<LidarrClient> logger, IOptions<LidarrInstanceOptions> options) : ArrClientBase()
{
private readonly string _lidarrHost = configuration.GetValue<string>("LIDARR_HOST") ?? throw new ArgumentException("LIDARR_HOST environment variable must be set");
private readonly string _lidarrApiKey = configuration.GetValue<string>("LIDARR_API_KEY") ?? throw new ArgumentException("LIDARR_API_KEY environment variable must be set");
public LidarrInstanceOptions LidarrOptions { get; } = options.Value;
private readonly string _mediaType = "audio";
public override async Task<IEnumerable<SearchItem>> FetchAllItemsAsync()
@@ -25,7 +25,7 @@ namespace UmlautAdaptarr.Providers
try
{
var lidarrArtistsUrl = $"{_lidarrHost}/api/v1/artist?apikey={_lidarrApiKey}";
var lidarrArtistsUrl = $"{LidarrOptions.Host}/api/v1/artist?apikey={LidarrOptions.ApiKey}";
logger.LogInformation($"Fetching all artists from Lidarr: {UrlUtilities.RedactApiKey(lidarrArtistsUrl)}");
var artistsApiResponse = await httpClient.GetStringAsync(lidarrArtistsUrl);
var artists = JsonConvert.DeserializeObject<List<dynamic>>(artistsApiResponse);
@@ -40,7 +40,7 @@ namespace UmlautAdaptarr.Providers
{
var artistId = (int)artist.id;
var lidarrAlbumUrl = $"{_lidarrHost}/api/v1/album?artistId={artistId}&apikey={_lidarrApiKey}";
var lidarrAlbumUrl = $"{LidarrOptions.Host}/api/v1/album?artistId={artistId}&apikey={LidarrOptions.ApiKey}";
// TODO add caching here
// Disable cache for now as it can result in problems when adding new albums that aren't displayed on the artists page initially
@@ -135,7 +135,7 @@ namespace UmlautAdaptarr.Providers
{
try
{
// this should never be called at the moment
// this should never be called at the moment
throw new NotImplementedException();
}
catch (Exception ex)

View File

@@ -1,7 +1,9 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UmlautAdaptarr.Models;
using UmlautAdaptarr.Options.ArrOptions;
using UmlautAdaptarr.Services;
using UmlautAdaptarr.Utilities;
@@ -9,13 +11,13 @@ namespace UmlautAdaptarr.Providers
{
public class ReadarrClient(
IHttpClientFactory clientFactory,
IConfiguration configuration,
CacheService cacheService,
IMemoryCache cache,
IOptions<ReadarrInstanceOptions> options,
ILogger<ReadarrClient> logger) : ArrClientBase()
{
private readonly string _readarrHost = configuration.GetValue<string>("READARR_HOST") ?? throw new ArgumentException("READARR_HOST environment variable must be set");
private readonly string _readarrApiKey = configuration.GetValue<string>("READARR_API_KEY") ?? throw new ArgumentException("READARR_API_KEY environment variable must be set");
public ReadarrInstanceOptions ReadarrOptions { get; } = options.Value;
private readonly string _mediaType = "book";
public override async Task<IEnumerable<SearchItem>> FetchAllItemsAsync()
@@ -25,7 +27,7 @@ namespace UmlautAdaptarr.Providers
try
{
var readarrAuthorUrl = $"{_readarrHost}/api/v1/author?apikey={_readarrApiKey}";
var readarrAuthorUrl = $"{ReadarrOptions.Host}/api/v1/author?apikey={ReadarrOptions.ApiKey}";
logger.LogInformation($"Fetching all authors from Readarr: {UrlUtilities.RedactApiKey(readarrAuthorUrl)}");
var authorApiResponse = await httpClient.GetStringAsync(readarrAuthorUrl);
var authors = JsonConvert.DeserializeObject<List<dynamic>>(authorApiResponse);
@@ -40,7 +42,7 @@ namespace UmlautAdaptarr.Providers
{
var authorId = (int)author.id;
var readarrBookUrl = $"{_readarrHost}/api/v1/book?authorId={authorId}&apikey={_readarrApiKey}";
var readarrBookUrl = $"{ReadarrOptions.Host}/api/v1/book?authorId={authorId}&apikey={ReadarrOptions.ApiKey}";
// TODO add caching here
logger.LogInformation($"Fetching all books from authorId {authorId} from Readarr: {UrlUtilities.RedactApiKey(readarrBookUrl)}");

View File

@@ -1,5 +1,7 @@
using Newtonsoft.Json;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using UmlautAdaptarr.Models;
using UmlautAdaptarr.Options.ArrOptions;
using UmlautAdaptarr.Services;
using UmlautAdaptarr.Utilities;
@@ -7,12 +9,11 @@ namespace UmlautAdaptarr.Providers
{
public class SonarrClient(
IHttpClientFactory clientFactory,
IConfiguration configuration,
TitleApiService titleService,
IOptions<SonarrInstanceOptions> options,
ILogger<SonarrClient> logger) : ArrClientBase()
{
private readonly string _sonarrHost = configuration.GetValue<string>("SONARR_HOST") ?? throw new ArgumentException("SONARR_HOST environment variable must be set");
private readonly string _sonarrApiKey = configuration.GetValue<string>("SONARR_API_KEY") ?? throw new ArgumentException("SONARR_API_KEY environment variable must be set");
public SonarrInstanceOptions SonarrOptions { get; } = options.Value;
private readonly string _mediaType = "tv";
public override async Task<IEnumerable<SearchItem>> FetchAllItemsAsync()
@@ -22,7 +23,7 @@ namespace UmlautAdaptarr.Providers
try
{
var sonarrUrl = $"{_sonarrHost}/api/v3/series?includeSeasonImages=false&apikey={_sonarrApiKey}";
var sonarrUrl = $"{SonarrOptions.Host}/api/v3/series?includeSeasonImages=false&apikey={SonarrOptions.ApiKey}";
logger.LogInformation($"Fetching all items from Sonarr: {UrlUtilities.RedactApiKey(sonarrUrl)}");
var response = await httpClient.GetStringAsync(sonarrUrl);
var shows = JsonConvert.DeserializeObject<List<dynamic>>(response);
@@ -71,7 +72,7 @@ namespace UmlautAdaptarr.Providers
try
{
var sonarrUrl = $"{_sonarrHost}/api/v3/series?tvdbId={externalId}&includeSeasonImages=false&apikey={_sonarrApiKey}";
var sonarrUrl = $"{SonarrOptions.Host}/api/v3/series?tvdbId={externalId}&includeSeasonImages=false&apikey={SonarrOptions.ApiKey}";
logger.LogInformation($"Fetching item by external ID from Sonarr: {UrlUtilities.RedactApiKey(sonarrUrl)}");
var response = await httpClient.GetStringAsync(sonarrUrl);
var shows = JsonConvert.DeserializeObject<dynamic>(response);
@@ -98,7 +99,7 @@ namespace UmlautAdaptarr.Providers
mediaType: _mediaType
);
logger.LogInformation($"Successfully fetched show {searchItem} from Sonarr.");
logger.LogInformation($"Successfully fetched show {searchItem.Title} from Sonarr.");
return searchItem;
}
}
@@ -123,7 +124,7 @@ namespace UmlautAdaptarr.Providers
return null;
}
var sonarrUrl = $"{_sonarrHost}/api/v3/series?tvdbId={tvdbId}&includeSeasonImages=false&apikey={_sonarrApiKey}";
var sonarrUrl = $"{SonarrOptions.Host}/api/v3/series?tvdbId={tvdbId}&includeSeasonImages=false&apikey={SonarrOptions.ApiKey}";
var sonarrApiResponse = await httpClient.GetStringAsync(sonarrUrl);
var shows = JsonConvert.DeserializeObject<dynamic>(sonarrApiResponse);
@@ -156,7 +157,7 @@ namespace UmlautAdaptarr.Providers
mediaType: _mediaType
);
logger.LogInformation($"Successfully fetched show {searchItem} from Sonarr.");
logger.LogInformation($"Successfully fetched show {searchItem.Title} from Sonarr.");
return searchItem;
}
catch (Exception ex)

View File

@@ -16,12 +16,8 @@ namespace UmlautAdaptarr.Services
LidarrClient lidarrClient,
ReadarrClient readarrClient,
CacheService cacheService,
IConfiguration configuration,
ILogger<ArrSyncBackgroundService> logger) : BackgroundService
{
private readonly bool _sonarrEnabled = configuration.GetValue<bool>("SONARR_ENABLED");
private readonly bool _lidarrEnabled = configuration.GetValue<bool>("LIDARR_ENABLED");
private readonly bool _readarrEnabled = configuration.GetValue<bool>("READARR_ENABLED");
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("ArrSyncBackgroundService is starting.");
@@ -62,17 +58,17 @@ namespace UmlautAdaptarr.Services
try
{
var success = true;
if (_readarrEnabled)
if (readarrClient.ReadarrOptions.Enabled)
{
var syncSuccess = await FetchItemsFromReadarrAsync();
success = success && syncSuccess;
}
if (_sonarrEnabled)
if (sonarrClient.SonarrOptions.Enabled)
{
var syncSuccess = await FetchItemsFromSonarrAsync();
success = success && syncSuccess;
}
if (_lidarrEnabled)
if (lidarrClient.LidarrOptions.Enabled)
{
var syncSuccess = await FetchItemsFromLidarrAsync();
success = success && syncSuccess;

View File

@@ -96,30 +96,51 @@ namespace UmlautAdaptarr.Services
// Use the first few characters of the normalized title for cache prefix search
var cacheSearchPrefix = normalizedTitle[..Math.Min(VARIATION_LOOKUP_CACHE_LENGTH, normalizedTitle.Length)];
SearchItem? bestSearchItemMatch = null;
var bestVariationMatchLength = 0;
HashSet<string> checkedSearchItems = [];
if (VariationIndex.TryGetValue(cacheSearchPrefix, out var cacheKeys))
{
foreach (var cacheKey in cacheKeys)
{
if (cache.TryGetValue(cacheKey, out SearchItem? item))
{
if (item?.MediaType != mediaType)
if (item == null || item.MediaType != mediaType)
{
continue;
}
var searchItemIdentifier = $"{item.MediaType}_{item.ExternalId}";
if (checkedSearchItems.Contains(searchItemIdentifier))
{
continue;
}
else
{
checkedSearchItems.Add(searchItemIdentifier);
}
// After finding a potential item, compare normalizedTitle with each German title variation
foreach (var variation in item?.TitleMatchVariations ?? [])
foreach (var variation in item.TitleMatchVariations ?? [])
{
var normalizedVariation = variation.RemoveAccentButKeepGermanUmlauts().ToLower();
if (normalizedTitle.StartsWith(variation, StringComparison.OrdinalIgnoreCase))
{
return item;
// If we find a variation match that is "longer" then most likely that one is correct and the earlier match was wrong (if it was from another searchItem)
if (variation.Length > bestVariationMatchLength)
{
bestSearchItemMatch = item;
bestVariationMatchLength = variation.Length;
}
}
}
}
}
}
return null;
return bestSearchItemMatch;
}
public SearchItem? GetSearchItemByExternalId(string mediaType, string externalId)

View File

@@ -0,0 +1,163 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace UmlautAdaptarr.Services
{
public class HttpProxyService : IHostedService
{
private TcpListener _listener;
private readonly ILogger<HttpProxyService> _logger;
private readonly int _proxyPort = 5006; // TODO move to appsettings.json
private readonly IHttpClientFactory _clientFactory;
public HttpProxyService(ILogger<HttpProxyService> logger, IHttpClientFactory clientFactory)
{
_logger = logger;
_clientFactory = clientFactory;
}
private async Task HandleRequests(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var clientSocket = await _listener.AcceptSocketAsync();
_ = Task.Run(() => ProcessRequest(clientSocket), stoppingToken);
}
}
private async Task ProcessRequest(Socket clientSocket)
{
using var clientStream = new NetworkStream(clientSocket, ownsSocket: true);
var buffer = new byte[8192];
var bytesRead = await clientStream.ReadAsync(buffer, 0, buffer.Length);
var requestString = Encoding.ASCII.GetString(buffer, 0, bytesRead);
if (requestString.StartsWith("CONNECT"))
{
// Handle HTTPS CONNECT request
await HandleHttpsConnect(requestString, clientStream, clientSocket);
}
else
{
// Handle HTTP request
await HandleHttp(requestString, clientStream, clientSocket, buffer, bytesRead);
}
}
private async Task HandleHttpsConnect(string requestString, NetworkStream clientStream, Socket clientSocket)
{
var targetInfo = ParseTargetInfo(requestString);
if (targetInfo.host != "prowlarr.servarr.com")
{
_logger.LogWarning($"IMPORTANT! {Environment.NewLine} Indexer {targetInfo.host} needs to be set to http:// instead of https:// {Environment.NewLine}" +
$"UmlautAdaptarr will not work for {targetInfo.host}!");
}
using var targetSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
await targetSocket.ConnectAsync(targetInfo.host, targetInfo.port);
await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 200 Connection Established\r\n\r\n"));
using var targetStream = new NetworkStream(targetSocket, ownsSocket: true);
await RelayTraffic(clientStream, targetStream);
}
catch (Exception ex)
{
_logger.LogError($"Failed to connect to target: {ex.Message}");
clientSocket.Close();
}
}
private async Task HandleHttp(string requestString, NetworkStream clientStream, Socket clientSocket, byte[] buffer, int bytesRead)
{
try
{
var headers = ParseHeaders(buffer, bytesRead);
string userAgent = headers.FirstOrDefault(h => h.Key == "User-Agent").Value;
var uri = new Uri(requestString.Split(' ')[1]);
var modifiedUri = $"http://localhost:5005/_/{uri.Host}{uri.PathAndQuery}"; // TODO read port from appsettings?
using var client = _clientFactory.CreateClient();
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, modifiedUri);
httpRequestMessage.Headers.Add("User-Agent", userAgent);
var result = await client.SendAsync(httpRequestMessage);
if (result.IsSuccessStatusCode)
{
var responseData = await result.Content.ReadAsByteArrayAsync();
await clientStream.WriteAsync(Encoding.ASCII.GetBytes($"HTTP/1.1 200 OK\r\nContent-Length: {responseData.Length}\r\n\r\n"));
await clientStream.WriteAsync(responseData);
}
else
{
await clientStream.WriteAsync(Encoding.ASCII.GetBytes($"HTTP/1.1 {result.StatusCode}\r\n\r\n"));
}
}
catch (Exception ex)
{
_logger.LogError($"HTTP Proxy error: {ex.Message}");
await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 500 Internal Server Error\r\n\r\n"));
}
finally
{
clientSocket.Close();
}
}
private Dictionary<string, string> ParseHeaders(byte[] buffer, int length)
{
var headers = new Dictionary<string, string>();
var headerString = Encoding.ASCII.GetString(buffer, 0, length);
var lines = headerString.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines.Skip(1)) // Skip the request line
{
var colonIndex = line.IndexOf(':');
if (colonIndex > 0)
{
var key = line.Substring(0, colonIndex).Trim();
var value = line.Substring(colonIndex + 1).Trim();
headers[key] = value;
}
}
return headers;
}
private (string host, int port) ParseTargetInfo(string requestLine)
{
var parts = requestLine.Split(' ')[1].Split(':');
return (parts[0], int.Parse(parts[1]));
}
private async Task RelayTraffic(NetworkStream clientStream, NetworkStream targetStream)
{
var clientToTargetTask = RelayStream(clientStream, targetStream);
var targetToClientTask = RelayStream(targetStream, clientStream);
await Task.WhenAll(clientToTargetTask, targetToClientTask);
}
private async Task RelayStream(NetworkStream input, NetworkStream output)
{
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = await input.ReadAsync(buffer.AsMemory(0, buffer.Length))) > 0)
{
await output.WriteAsync(buffer.AsMemory(0, bytesRead));
await output.FlushAsync();
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
_listener = new TcpListener(IPAddress.Any, _proxyPort);
_listener.Start();
Task.Run(() => HandleRequests(cancellationToken), cancellationToken);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_listener.Stop();
return Task.CompletedTask;
}
}
}

View File

@@ -1,21 +1,26 @@
using Microsoft.Extensions.Caching.Memory;
using System.Collections.Concurrent;
using Microsoft.Extensions.Options;
using UmlautAdaptarr.Options;
using UmlautAdaptarr.Utilities;
namespace UmlautAdaptarr.Services
{
public class ProxyService
public class ProxyRequestService
{
private readonly HttpClient _httpClient;
private readonly string _userAgent;
private readonly ILogger<ProxyService> _logger;
private readonly ILogger<ProxyRequestService> _logger;
private readonly IMemoryCache _cache;
private readonly GlobalOptions _options;
private static readonly ConcurrentDictionary<string, DateTimeOffset> _lastRequestTimes = new();
private static readonly TimeSpan MINIMUM_DELAY_FOR_SAME_HOST = new(0, 0, 0, 1);
public ProxyService(IHttpClientFactory clientFactory, IConfiguration configuration, ILogger<ProxyService> logger, IMemoryCache cache)
public ProxyRequestService(IHttpClientFactory clientFactory, ILogger<ProxyRequestService> logger, IMemoryCache cache, IOptions<GlobalOptions> options)
{
_options = options.Value;
_httpClient = clientFactory.CreateClient("HttpClient") ?? throw new ArgumentNullException(nameof(clientFactory));
_userAgent = configuration["Settings:UserAgent"] ?? throw new ArgumentException("UserAgent must be set in appsettings.json");
_userAgent = _options.UserAgent ?? throw new ArgumentException("UserAgent must be set in appsettings.json");
_logger = logger;
_cache = cache;
}
@@ -26,9 +31,9 @@ namespace UmlautAdaptarr.Services
if (_lastRequestTimes.TryGetValue(host, out var lastRequestTime))
{
var timeSinceLastRequest = DateTimeOffset.Now - lastRequestTime;
if (timeSinceLastRequest < TimeSpan.FromMilliseconds(1500))
if (timeSinceLastRequest < MINIMUM_DELAY_FOR_SAME_HOST)
{
await Task.Delay(TimeSpan.FromMilliseconds(1500) - timeSinceLastRequest);
await Task.Delay(MINIMUM_DELAY_FOR_SAME_HOST - timeSinceLastRequest);
}
}
_lastRequestTimes[host] = DateTimeOffset.Now;
@@ -71,12 +76,12 @@ namespace UmlautAdaptarr.Services
try
{
_logger.LogInformation($"ProxyService GET {UrlUtilities.RedactApiKey(targetUri)}");
_logger.LogInformation($"ProxyRequestService GET {UrlUtilities.RedactApiKey(targetUri)}");
var responseMessage = await _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
if (responseMessage.IsSuccessStatusCode)
{
_cache.Set(targetUri, responseMessage, TimeSpan.FromMinutes(5));
_cache.Set(targetUri, responseMessage, TimeSpan.FromMinutes(12));
}
return responseMessage;
@@ -85,7 +90,6 @@ namespace UmlautAdaptarr.Services
{
_logger.LogError(ex, $"Error proxying request: {UrlUtilities.RedactApiKey(targetUri)}. Error: {ex.Message}");
// Create a response message indicating an internal server error
var errorResponse = new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError)
{
Content = new StringContent($"An error occurred while processing your request: {ex.Message}")

View File

@@ -6,12 +6,8 @@ namespace UmlautAdaptarr.Services
public class SearchItemLookupService(CacheService cacheService,
SonarrClient sonarrClient,
ReadarrClient readarrClient,
LidarrClient lidarrClient,
IConfiguration configuration)
LidarrClient lidarrClient)
{
private readonly bool _sonarrEnabled = configuration.GetValue<bool>("SONARR_ENABLED");
private readonly bool _lidarrEnabled = configuration.GetValue<bool>("LIDARR_ENABLED");
private readonly bool _readarrEnabled = configuration.GetValue<bool>("READARR_ENABLED");
public async Task<SearchItem?> GetOrFetchSearchItemByExternalId(string mediaType, string externalId)
{
// Attempt to get the item from the cache first
@@ -26,20 +22,20 @@ namespace UmlautAdaptarr.Services
switch (mediaType)
{
case "tv":
if (_sonarrEnabled)
if (sonarrClient.SonarrOptions.Enabled)
{
fetchedItem = await sonarrClient.FetchItemByExternalIdAsync(externalId);
}
break;
case "audio":
if (_lidarrEnabled)
if (lidarrClient.LidarrOptions.Enabled)
{
fetchedItem = await lidarrClient.FetchItemByExternalIdAsync(externalId);
await lidarrClient.FetchItemByExternalIdAsync(externalId);
fetchedItem = cacheService.GetSearchItemByExternalId(mediaType, externalId);
}
break;
case "book":
if (_readarrEnabled)
if (readarrClient.ReadarrOptions.Enabled)
{
await readarrClient.FetchItemByExternalIdAsync(externalId);
fetchedItem = cacheService.GetSearchItemByExternalId(mediaType, externalId);
@@ -70,7 +66,7 @@ namespace UmlautAdaptarr.Services
switch (mediaType)
{
case "tv":
if (_sonarrEnabled)
if (sonarrClient.SonarrOptions.Enabled)
{
fetchedItem = await sonarrClient.FetchItemByTitleAsync(title);
}

View File

@@ -1,13 +1,15 @@
using Newtonsoft.Json;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UmlautAdaptarr.Options;
using UmlautAdaptarr.Utilities;
namespace UmlautAdaptarr.Services
{
public class TitleApiService(IHttpClientFactory clientFactory, IConfiguration configuration, ILogger<TitleApiService> logger)
public class TitleApiService(IHttpClientFactory clientFactory, ILogger<TitleApiService> logger, IOptions<GlobalOptions> options)
{
private readonly string _umlautAdaptarrApiHost = configuration["Settings:UmlautAdaptarrApiHost"]
?? throw new ArgumentException("UmlautAdaptarrApiHost must be set in appsettings.json");
public GlobalOptions Options { get; } = options.Value;
private DateTime lastRequestTime = DateTime.MinValue;
private async Task EnsureMinimumDelayAsync()
@@ -28,7 +30,7 @@ namespace UmlautAdaptarr.Services
await EnsureMinimumDelayAsync();
var httpClient = clientFactory.CreateClient();
var titleApiUrl = $"{_umlautAdaptarrApiHost}/tvshow_german.php?tvdbid={externalId}";
var titleApiUrl = $"{Options.UmlautAdaptarrApiHost}/tvshow_german.php?tvdbid={externalId}";
logger.LogInformation($"TitleApiService GET {UrlUtilities.RedactApiKey(titleApiUrl)}");
var response = await httpClient.GetStringAsync(titleApiUrl);
var titleApiResponseData = JsonConvert.DeserializeObject<dynamic>(response);
@@ -74,7 +76,7 @@ namespace UmlautAdaptarr.Services
var httpClient = clientFactory.CreateClient();
var tvdbCleanTitle = title.Replace("ß", "ss");
var titleApiUrl = $"{_umlautAdaptarrApiHost}/tvshow_german.php?title={tvdbCleanTitle}";
var titleApiUrl = $"{Options.UmlautAdaptarrApiHost}/tvshow_german.php?title={tvdbCleanTitle}";
logger.LogInformation($"TitleApiService GET {UrlUtilities.RedactApiKey(titleApiUrl)}");
var titleApiResponse = await httpClient.GetStringAsync(titleApiUrl);
var titleApiResponseData = JsonConvert.DeserializeObject<dynamic>(titleApiResponse);

View File

@@ -228,7 +228,7 @@ namespace UmlautAdaptarr.Services
titleElement.Value = newTitle;
logger.LogInformation($"TitleMatchingService - Title changed: '{originalTitle}' to '{newTitle}'");
break; // Break after the first successful match and modification
break;
}
}
}
@@ -242,7 +242,7 @@ namespace UmlautAdaptarr.Services
private static char FindFirstSeparator(string title)
{
var match = WordSeperationCharRegex().Match(title);
return match.Success ? match.Value.First() : ' '; // Default to space if no separator found
return match.Success ? match.Value.First() : ' ';
}
private static string ReconstructTitleWithSeparator(string title, char separator)
@@ -262,23 +262,23 @@ namespace UmlautAdaptarr.Services
return null;
}
if (category.StartsWith("EBook", StringComparison.OrdinalIgnoreCase) || category.StartsWith("Book", StringComparison.OrdinalIgnoreCase))
if (category == "7000" || category.StartsWith("EBook", StringComparison.OrdinalIgnoreCase) || category.StartsWith("Book", StringComparison.OrdinalIgnoreCase))
{
return "book";
}
else if (category.StartsWith("Movies", StringComparison.OrdinalIgnoreCase))
else if (category == "2000" || category.StartsWith("Movies", StringComparison.OrdinalIgnoreCase))
{
return "movies";
}
else if (category.StartsWith("TV", StringComparison.OrdinalIgnoreCase))
else if (category == "5000" || category.StartsWith("TV", StringComparison.OrdinalIgnoreCase))
{
return "tv";
}
else if (category.Contains("Audiobook", StringComparison.OrdinalIgnoreCase))
else if (category == "3030" || category.Contains("Audiobook", StringComparison.OrdinalIgnoreCase))
{
return "book";
}
else if (category.StartsWith("Audio"))
else if (category == "3000" || category.StartsWith("Audio"))
{
return "audio";
}

View File

@@ -1,162 +0,0 @@
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
using UmlautAdaptarr.Models;
using UmlautAdaptarr.Providers;
using UmlautAdaptarr.Utilities;
namespace UmlautAdaptarr.Services
{
public class TitleQueryServiceLegacy(
IMemoryCache memoryCache,
ILogger<TitleQueryServiceLegacy> logger,
IConfiguration configuration,
IHttpClientFactory clientFactory,
SonarrClient sonarrClient)
{
private readonly HttpClient _httpClient = clientFactory.CreateClient("HttpClient") ?? throw new ArgumentNullException();
private readonly string _sonarrHost = configuration.GetValue<string>("SONARR_HOST") ?? throw new ArgumentException("SONARR_HOST environment variable must be set");
private readonly string _sonarrApiKey = configuration.GetValue<string>("SONARR_API_KEY") ?? throw new ArgumentException("SONARR_API_KEY environment variable must be set");
private readonly string _umlautAdaptarrApiHost = configuration["Settings:UmlautAdaptarrApiHost"] ?? throw new ArgumentException("UmlautAdaptarrApiHost must be set in appsettings.json");
/*public async Task<(bool hasGermanUmlaut, string? GermanTitle, string ExpectedTitle)> QueryGermanShowTitleByTVDBId(string tvdbId)
{
var sonarrCacheKey = $"SearchItem_Sonarr_{tvdbId}";
if (memoryCache.TryGetValue(sonarrCacheKey, out SearchItem? cachedItem))
{
return (cachedItem?.HasGermanUmlaut ?? false, cachedItem?.GermanTitle, cachedItem?.ExpectedTitle ?? string.Empty);
}
else
{
var sonarrUrl = $"{_sonarrHost}/api/v3/series?tvdbId={tvdbId}&includeSeasonImages=false&apikey={_sonarrApiKey}";
var sonarrApiResponse = await _httpClient.GetStringAsync(sonarrUrl);
var shows = JsonConvert.DeserializeObject<dynamic>(sonarrApiResponse);
if (shows == null)
{
logger.LogError($"Parsing Sonarr API response for TVDB ID {tvdbId} resulted in null");
return (false, null, string.Empty);
}
else if (shows.Count == 0)
{
logger.LogWarning($"No results found for TVDB ID {tvdbId}");
return (false, null, string.Empty);
}
var expectedTitle = (string)shows[0].title;
if (expectedTitle == null)
{
logger.LogError($"Sonarr Title for TVDB ID {tvdbId} is null");
return (false, null, string.Empty);
}
string? germanTitle = null;
var hasGermanTitle = false;
var titleApiUrl = $"{_umlautAdaptarrApiHost}/tvshow_german.php?tvdbid={tvdbId}";
var titleApiResponse = await _httpClient.GetStringAsync(titleApiUrl);
var titleApiResponseData = JsonConvert.DeserializeObject<dynamic>(titleApiResponse);
if (titleApiResponseData == null)
{
logger.LogError($"Parsing UmlautAdaptarr TitleQuery API response for TVDB ID {tvdbId} resulted in null");
return (false, null, string.Empty);
}
if (titleApiResponseData.status == "success" && !string.IsNullOrEmpty((string)titleApiResponseData.germanTitle))
{
germanTitle = titleApiResponseData.germanTitle;
hasGermanTitle = true;
}
var hasGermanUmlaut = germanTitle?.HasGermanUmlauts() ?? false;
var result = (hasGermanUmlaut, germanTitle, expectedTitle);
memoryCache.Set(showCacheKey, result, new MemoryCacheEntryOptions
{
Size = 1,
SlidingExpiration = hasGermanTitle ? TimeSpan.FromDays(30) : TimeSpan.FromDays(7)
});
return result;
}
}*/
// This method is being used if the *arrs do a search with the "q" parameter (text search)
public async Task<(bool hasGermanUmlaut, string? GermanTitle, string ExpectedTitle)> QueryGermanShowTitleByTitle(string title)
{
// TVDB doesn't use ß - TODO: Determine if this is true
var tvdbCleanTitle = title.Replace("ß", "ss");
var cacheKey = $"show_{tvdbCleanTitle}";
if (memoryCache.TryGetValue(cacheKey, out (bool hasGermanUmlaut, string? GermanTitle, string ExpectedTitle) cachedResult))
{
return cachedResult;
}
var titleApiUrl = $"{_umlautAdaptarrApiHost}/tvshow_german.php?title={tvdbCleanTitle}";
var titleApiResponse = await _httpClient.GetStringAsync(titleApiUrl);
var titleApiResponseData = JsonConvert.DeserializeObject<dynamic>(titleApiResponse);
if (titleApiResponseData == null)
{
logger.LogError($"Parsing UmlautAdaptarr TitleQuery API response for title {title} resulted in null");
return (false, null, string.Empty);
}
if (titleApiResponseData.status == "success" && !string.IsNullOrEmpty((string)titleApiResponseData.germanTitle))
{
var tvdbId = (string)titleApiResponseData.tvdbId;
if (tvdbId == null)
{
logger.LogError($"Parsing UmlautAdaptarr TitleQuery API response tvdbId {titleApiResponseData} resulted in null");
return (false, null, string.Empty);
}
var sonarrUrl = $"{_sonarrHost}/api/v3/series?tvdbId={tvdbId}&includeSeasonImages=false&apikey={_sonarrApiKey}";
var sonarrApiResponse = await _httpClient.GetStringAsync(sonarrUrl);
var shows = JsonConvert.DeserializeObject<dynamic>(sonarrApiResponse);
if (shows == null)
{
logger.LogError($"Parsing Sonarr API response for TVDB ID {tvdbId} resulted in null");
return (false, null, string.Empty);
}
else if (shows.Count == 0)
{
logger.LogWarning($"No results found for TVDB ID {tvdbId}");
return (false, null, string.Empty);
}
var expectedTitle = (string)shows[0].title;
if (expectedTitle == null)
{
logger.LogError($"Sonarr Title for TVDB ID {tvdbId} is null");
return (false, null, string.Empty);
}
string germanTitle ;
bool hasGermanTitle;
germanTitle = titleApiResponseData.germanTitle;
hasGermanTitle = true;
var hasGermanUmlaut = germanTitle?.HasUmlauts() ?? false;
var result = (hasGermanUmlaut, germanTitle, expectedTitle);
memoryCache.Set(cacheKey, result, new MemoryCacheEntryOptions
{
Size = 1,
SlidingExpiration = hasGermanTitle ? TimeSpan.FromDays(30) : TimeSpan.FromDays(7)
});
return result;
}
else
{
logger.LogWarning($"UmlautAdaptarr TitleQuery {titleApiUrl} didn't succeed.");
return (false, null, string.Empty);
}
}
}
}

View File

@@ -9,6 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

View File

@@ -0,0 +1,19 @@
namespace UmlautAdaptarr.Utilities
{
/// <summary>
/// Service for providing a static logger to log errors and information.
/// The GlobalStaticLogger is designed to provide a static logger that can be used to log errors and information.
/// It facilitates logging for both static classes and extension methods.
/// </summary>
public static class GlobalStaticLogger
{
public static ILogger Logger;
/// <summary>
/// Initializes the GlobalStaticLogger with the provided logger factory.
/// </summary>
/// <param name="loggerFactory">The ILoggerFactory instance used to create loggers.</param>
public static void Initialize(ILoggerFactory loggerFactory) => Logger = loggerFactory.CreateLogger("GlobalStaticLogger");
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Net;
using UmlautAdaptarr.Options;
namespace UmlautAdaptarr.Utilities
{
/// <summary>
/// Extension methods for configuring proxies.
/// </summary>
public static class ProxyExtension
{
/// <summary>
/// Logger instance for logging proxy configurations.
/// </summary>
private static ILogger Logger = GlobalStaticLogger.Logger;
/// <summary>
/// Configures the proxy settings for the provided HttpClientHandler instance.
/// </summary>
/// <param name="handler">The HttpClientHandler instance to configure.</param>
/// <param name="proxyOptions">ProxyOptions options to be used for configuration.</param>
/// <returns>The configured HttpClientHandler instance.</returns>
public static HttpClientHandler ConfigureProxy(this HttpClientHandler handler, ProxyOptions? proxyOptions)
{
try
{
if (proxyOptions != null && proxyOptions.Enabled)
{
Logger.LogInformation("Use Proxy {0}", proxyOptions.Address);
handler.UseProxy = true;
handler.Proxy = new WebProxy(proxyOptions.Address, proxyOptions.BypassOnLocal);
if (!string.IsNullOrEmpty(proxyOptions.Username) && !string.IsNullOrEmpty(proxyOptions.Password))
{
Logger.LogInformation("Use Proxy Credentials from User {0}", proxyOptions.Username);
handler.DefaultProxyCredentials =
new NetworkCredential(proxyOptions.Username, proxyOptions.Password);
}
}
else
{
Logger.LogDebug("No proxy was set");
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error occurred while configuring proxy, no Proxy will be used!");
}
return handler;
}
}
}

View File

@@ -0,0 +1,92 @@
using UmlautAdaptarr.Options;
using UmlautAdaptarr.Options.ArrOptions;
using UmlautAdaptarr.Providers;
using UmlautAdaptarr.Services;
namespace UmlautAdaptarr.Utilities
{
/// <summary>
/// Extension methods for configuring services related to ARR Applications
/// </summary>
public static class ServicesExtensions
{
/// <summary>
/// Adds a service with specified options and service to the service collection.
/// </summary>
/// <typeparam name="TOptions">The options type for the service.</typeparam>
/// <typeparam name="TService">The service type for the service.</typeparam>
/// <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>
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
private static WebApplicationBuilder AddServiceWithOptions<TOptions, TService>(this WebApplicationBuilder builder, string sectionName)
where TOptions : class
where TService : class
{
if (builder.Services == null)
{
throw new ArgumentNullException(nameof(builder), "Service collection is null.");
}
var options = builder.Configuration.GetSection(sectionName).Get<TOptions>();
if (options == null)
{
throw new InvalidOperationException($"{typeof(TService).Name} options could not be loaded from Configuration or ENV Variable.");
}
builder.Services.Configure<TOptions>(builder.Configuration.GetSection(sectionName));
builder.Services.AddSingleton<TService>();
return builder;
}
/// <summary>
/// Adds support for Sonarr with default options and client.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
public static WebApplicationBuilder AddSonarrSupport(this WebApplicationBuilder builder)
{
return builder.AddServiceWithOptions<SonarrInstanceOptions, SonarrClient>("Sonarr");
}
/// <summary>
/// Adds support for Lidarr with default options and client.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
public static WebApplicationBuilder AddLidarrSupport(this WebApplicationBuilder builder)
{
return builder.AddServiceWithOptions<LidarrInstanceOptions, LidarrClient>("Lidarr");
}
/// <summary>
/// Adds support for Readarr with default options and client.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
public static WebApplicationBuilder AddReadarrSupport(this WebApplicationBuilder builder)
{
return builder.AddServiceWithOptions<ReadarrInstanceOptions, ReadarrClient>("Readarr");
}
/// <summary>
/// Adds a title lookup service to the service collection.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
public static WebApplicationBuilder AddTitleLookupService(this WebApplicationBuilder builder)
{
return builder.AddServiceWithOptions<GlobalOptions, TitleApiService>("Settings");
}
/// <summary>
/// Adds a proxy request service to the service collection.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
public static WebApplicationBuilder AddProxyRequestService(this WebApplicationBuilder builder)
{
return builder.AddServiceWithOptions<GlobalOptions, ProxyRequestService>("Settings");
}
}
}

View File

@@ -12,7 +12,7 @@ namespace UmlautAdaptarr.Utilities
// RegEx für eine einfache URL-Validierung ohne http:// und ohne abschließenden Schrägstrich
// Erlaubt optionale Subdomains, Domainnamen und TLDs, aber keine Pfade oder Protokolle
var regex = UrlMatchingRegex();
return regex.IsMatch(domain) && !domain.EndsWith("/");
return regex.IsMatch(domain);
}
public static string BuildUrl(string domain, IDictionary<string, string> queryParameters)

View File

@@ -3,18 +3,64 @@
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
},
"Console": {
"TimestampFormat": "yyyy-MM-dd HH:mm:ss::"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://*:5005"
"Url": "http://[::]:5005"
}
}
},
// Settings__UserAgent=UmlautAdaptarr/1.0
// Settings__UmlautAdaptarrApiHost=https://umlautadaptarr.pcjones.de/api/v1
"Settings": {
"UserAgent": "UmlautAdaptarr/1.0",
"UmlautAdaptarrApiHost": "https://umlautadaptarr.pcjones.de/api/v1"
},
"Sonarr": {
// Docker Environment Variables:
// - Sonarr__Enabled: true (set to false to disable)
// - Sonarr__Host: your_sonarr_host_url
// - Sonarr__ApiKey: your_sonarr_api_key
"Enabled": false,
"Host": "your_sonarr_host_url",
"ApiKey": "your_sonarr_api_key"
},
"Lidarr": {
// Docker Environment Variables:
// - Lidarr__Enabled: true (set to false to disable)
// - Lidarr__Host: your_lidarr_host_url
// - Lidarr__ApiKey: your_lidarr_api_key
"Enabled": false,
"Host": "your_lidarr_host_url",
"ApiKey": "your_lidarr_api_key"
},
"Readarr": {
// Docker Environment Variables:
// - Readarr__Enabled: true (set to false to disable)
// - Readarr__Host: your_readarr_host_url
// - Readarr__ApiKey: your_readarr_api_key
"Enabled": false,
"Host": "your_readarr_host_url",
"ApiKey": "your_readarr_api_key"
},
// Docker Environment Variables:
// - Proxy__Enabled: true (set to false to disable)
// - Proxy__Address: http://yourproxyaddress:port
// - Proxy__Username: your_proxy_username
// - Proxy__Password: your_proxy_password
// - Proxy__BypassOnLocal: true (set to false to not bypass local IP addresses)
"Proxy": {
"Enabled": false,
"Address": "http://yourproxyaddress:port",
"Username": "your_proxy_username",
"Password": "your_proxy_password",
"BypassOnLocal": true
}
}

View File

@@ -1,11 +0,0 @@
{
"SONARR_ENABLED": false,
"SONARR_HOST": "http://localhost:8989",
"SONARR_API_KEY": "",
"LIDARR_ENABLED": false,
"LIDARR_HOST": "http://localhost:8686",
"LIDARR_API_KEY": "",
"READARR_ENABLED": false,
"READARR_HOST": "http://localhost:8787",
"READARR_API_KEY": ""
}

View File

@@ -8,17 +8,20 @@ services:
restart: unless-stopped
environment:
- TZ=Europe/Berlin
- SONARR_ENABLED=false
- SONARR_HOST=http://localhost:8989
- SONARR_API_KEY=API_KEY
- RADARR_ENABLED=false
- RADARR_HOST=http://localhost:7878
- RADARR_API_KEY=API_KEY
- READARR_ENABLED=false
- READARR_HOST=http://localhost:8787
- READARR_API_KEY=API_KEY
- LIDARR_ENABLED=false
- LIDARR_HOST=http://localhost:8686
- LIDARR_API_KEY=API_KEY
ports:
- "5005":"5005"
- SONARR__ENABLED=false
- SONARR__HOST=http://localhost:8989
- SONARR__APIKEY=APIKEY
- RADARR__ENABLED=false
- RADARR__HOST=http://localhost:7878
- RADARR__APIKEY=APIKEY
- READARR__ENABLED=false
- READARR__HOST=http://localhost:8787
- READARR__APIKEY=APIKEY
- LIDARR__ENABLED=false
- LIDARR__HOST=http://localhost:8686
- LIDARR__APIKEY=APIKEY
#- Proxy__Enabled: false
#- Proxy__Address: http://yourproxyaddress:port
#- Proxy__Username: your_proxy_username
#- Proxy__Password: your_proxy_password
#- Proxy__BypassOnLocal: true (set to false to not bypass local IP addresses)