Intermediate commit
This commit is contained in:
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Use the official Microsoft .NET Core SDK image as the build environment
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy everything and build the project
|
||||||
|
COPY . ./
|
||||||
|
RUN dotnet restore
|
||||||
|
RUN dotnet publish -c Release -o out
|
||||||
|
|
||||||
|
# Generate the runtime image
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-env /app/out .
|
||||||
|
ENTRYPOINT ["dotnet", "UmlautAdaptarr.dll"]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using UmlautAdaptarr.Models;
|
using UmlautAdaptarr.Models;
|
||||||
@@ -9,15 +10,12 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
{
|
{
|
||||||
public abstract class SearchControllerBase(ProxyService proxyService, TitleMatchingService titleMatchingService) : ControllerBase
|
public abstract class SearchControllerBase(ProxyService proxyService, TitleMatchingService titleMatchingService) : ControllerBase
|
||||||
{
|
{
|
||||||
protected readonly ProxyService _proxyService = proxyService;
|
private readonly bool TODO_FORCE_TEXT_SEARCH_ORIGINAL_TITLE = false;
|
||||||
protected readonly TitleMatchingService _titleMatchingService = titleMatchingService;
|
private readonly bool TODO_FORCE_TEXT_SEARCH_GERMAN_TITLE = false;
|
||||||
|
|
||||||
protected async Task<IActionResult> BaseSearch(string options,
|
protected async Task<IActionResult> BaseSearch(string options,
|
||||||
string domain,
|
string domain,
|
||||||
IDictionary<string, string> queryParameters,
|
IDictionary<string, string> queryParameters,
|
||||||
string? germanTitle = null,
|
SearchItem? searchItem = null)
|
||||||
string? expectedTitle = null,
|
|
||||||
bool hasGermanUmlaut = false)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -26,37 +24,66 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
return NotFound($"{domain} is not a valid URL.");
|
return NotFound($"{domain} is not a valid URL.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate title variations for renaming process
|
var initialSearchResult = await PerformSingleSearchRequest(domain, queryParameters) as ContentResult;
|
||||||
var germanTitleVariations = !string.IsNullOrEmpty(germanTitle) ? _titleMatchingService.GenerateTitleVariations(germanTitle) : new List<string>();
|
if (initialSearchResult == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if "q" parameter exists for multiple search request handling
|
string inititalProcessedContent = string.Empty;
|
||||||
if (hasGermanUmlaut && !string.IsNullOrEmpty(germanTitle) && !string.IsNullOrEmpty(expectedTitle) && queryParameters.ContainsKey("q"))
|
// Rename titles in the single search content
|
||||||
|
if (!string.IsNullOrEmpty(initialSearchResult?.Content))
|
||||||
{
|
{
|
||||||
|
inititalProcessedContent = ProcessContent(initialSearchResult.Content, searchItem?.TitleMatchVariations, searchItem?.ExpectedTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
var additionalTextSearch = searchItem != null
|
||||||
|
&& !string.IsNullOrEmpty(searchItem.ExpectedTitle)
|
||||||
|
&& (TODO_FORCE_TEXT_SEARCH_GERMAN_TITLE || TODO_FORCE_TEXT_SEARCH_ORIGINAL_TITLE ||
|
||||||
|
// TODO check if this is a good idea
|
||||||
|
(searchItem.TitleSearchVariations.Length > 0 && !(searchItem.TitleSearchVariations.Length == 1 && searchItem.TitleSearchVariations[0] == searchItem.ExpectedTitle)));
|
||||||
|
|
||||||
|
if (additionalTextSearch)
|
||||||
|
{
|
||||||
|
// Aggregate the initial search result with additional results
|
||||||
|
// Remove identifiers for subsequent searches
|
||||||
|
// TODO rework this
|
||||||
|
queryParameters.Remove("tvdbid");
|
||||||
|
queryParameters.Remove("tvmazeid");
|
||||||
|
queryParameters.Remove("imdbid");
|
||||||
|
|
||||||
|
var titleSearchVariations = new List<string>(searchItem?.TitleSearchVariations);
|
||||||
|
|
||||||
|
string searchQuery = string.Empty;
|
||||||
|
if (queryParameters.TryGetValue("q", out string? q))
|
||||||
|
{
|
||||||
|
searchQuery = q ?? string.Empty;
|
||||||
// Add original search query to title variations
|
// Add original search query to title variations
|
||||||
var q = queryParameters["q"];
|
if (!titleSearchVariations.Remove(searchQuery))
|
||||||
if (!germanTitleVariations.Contains(q))
|
|
||||||
{
|
{
|
||||||
germanTitleVariations.Add(queryParameters["q"]!);
|
titleSearchVariations.Add(searchQuery);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedTitle = searchItem.ExpectedTitle;
|
||||||
|
|
||||||
|
if (TODO_FORCE_TEXT_SEARCH_ORIGINAL_TITLE)
|
||||||
|
{
|
||||||
|
if (expectedTitle != searchQuery && !titleSearchVariations.Contains(expectedTitle))
|
||||||
|
{
|
||||||
|
titleSearchVariations.Add(expectedTitle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle multiple search requests based on German title variations
|
// Handle multiple search requests based on German title variations
|
||||||
var aggregatedResult = await AggregateSearchResults(domain, queryParameters, germanTitleVariations, expectedTitle);
|
var aggregatedResult = await AggregateSearchResults(domain, queryParameters, titleSearchVariations, searchItem.TitleMatchVariations, expectedTitle);
|
||||||
// Rename titles in the aggregated content
|
aggregatedResult.AggregateItems(inititalProcessedContent);
|
||||||
var processedContent = ProcessContent(aggregatedResult.Content, germanTitleVariations, expectedTitle);
|
|
||||||
return Content(processedContent, aggregatedResult.ContentType, aggregatedResult.ContentEncoding);
|
return Content(aggregatedResult.Content, aggregatedResult.ContentType, aggregatedResult.ContentEncoding);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var singleSearchResult = await PerformSingleSearchRequest(domain, queryParameters);
|
|
||||||
// Rename titles in the single search content
|
|
||||||
var contentResult = singleSearchResult as ContentResult;
|
|
||||||
if (contentResult != null)
|
|
||||||
{
|
|
||||||
var processedContent = ProcessContent(contentResult.Content ?? "", germanTitleVariations, expectedTitle);
|
|
||||||
return Content(processedContent, contentResult.ContentType!, Encoding.UTF8);
|
|
||||||
}
|
|
||||||
return singleSearchResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialSearchResult!.Content = inititalProcessedContent;
|
||||||
|
return initialSearchResult;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -70,7 +97,7 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
private async Task<IActionResult> PerformSingleSearchRequest(string domain, IDictionary<string, string> queryParameters)
|
private async Task<IActionResult> PerformSingleSearchRequest(string domain, IDictionary<string, string> queryParameters)
|
||||||
{
|
{
|
||||||
var requestUrl = UrlUtilities.BuildUrl(domain, queryParameters);
|
var requestUrl = UrlUtilities.BuildUrl(domain, queryParameters);
|
||||||
var responseMessage = await _proxyService.ProxyRequestAsync(HttpContext, requestUrl);
|
var responseMessage = await proxyService.ProxyRequestAsync(HttpContext, requestUrl);
|
||||||
var content = await responseMessage.Content.ReadAsStringAsync();
|
var content = await responseMessage.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
var encoding = responseMessage.Content.Headers.ContentType?.CharSet != null ?
|
var encoding = responseMessage.Content.Headers.ContentType?.CharSet != null ?
|
||||||
@@ -82,18 +109,17 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private string ProcessContent(string content, List<string> germanTitleVariations, string? expectedTitle)
|
private string ProcessContent(string content, string[]? titleMatchVariations = null, string? expectedTitle = null)
|
||||||
{
|
{
|
||||||
// Check if German title and expected title are provided for renaming
|
return titleMatchingService.RenameTitlesInContent(content, titleMatchVariations, expectedTitle);
|
||||||
if (!string.IsNullOrEmpty(expectedTitle) && germanTitleVariations.Count != 0)
|
|
||||||
{
|
|
||||||
// Process and rename titles in the content
|
|
||||||
content = _titleMatchingService.RenameTitlesInContent(content, germanTitleVariations, expectedTitle);
|
|
||||||
}
|
|
||||||
return content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AggregatedSearchResult> AggregateSearchResults(string domain, IDictionary<string, string> queryParameters, List<string> germanTitleVariations, string expectedTitle)
|
public async Task<AggregatedSearchResult> AggregateSearchResults(
|
||||||
|
string domain,
|
||||||
|
IDictionary<string, string> queryParameters,
|
||||||
|
IEnumerable<string> titleSearchVariations,
|
||||||
|
string[] titleMatchVariations,
|
||||||
|
string expectedTitle)
|
||||||
{
|
{
|
||||||
string defaultContentType = "application/xml";
|
string defaultContentType = "application/xml";
|
||||||
Encoding defaultEncoding = Encoding.UTF8;
|
Encoding defaultEncoding = Encoding.UTF8;
|
||||||
@@ -101,23 +127,23 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
|
|
||||||
var aggregatedResult = new AggregatedSearchResult(defaultContentType, defaultEncoding);
|
var aggregatedResult = new AggregatedSearchResult(defaultContentType, defaultEncoding);
|
||||||
|
|
||||||
foreach (var titleVariation in germanTitleVariations.Distinct())
|
foreach (var titleVariation in titleSearchVariations)
|
||||||
{
|
{
|
||||||
queryParameters["q"] = titleVariation; // Replace the "q" parameter for each variation
|
queryParameters["q"] = titleVariation; // Replace the "q" parameter for each variation
|
||||||
var requestUrl = UrlUtilities.BuildUrl(domain, queryParameters);
|
var requestUrl = UrlUtilities.BuildUrl(domain, queryParameters);
|
||||||
var responseMessage = await _proxyService.ProxyRequestAsync(HttpContext, requestUrl);
|
var responseMessage = await proxyService.ProxyRequestAsync(HttpContext, requestUrl);
|
||||||
var content = await responseMessage.Content.ReadAsStringAsync();
|
var content = await responseMessage.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
// Only update encoding from the first response
|
// Only update encoding from the first response
|
||||||
if (!encodingSet && responseMessage.Content.Headers.ContentType?.CharSet != null)
|
if (!encodingSet && responseMessage.Content.Headers.ContentType?.CharSet != null)
|
||||||
{
|
{
|
||||||
aggregatedResult.ContentEncoding = Encoding.GetEncoding(responseMessage.Content.Headers.ContentType.CharSet); ;
|
aggregatedResult.ContentEncoding = Encoding.GetEncoding(responseMessage.Content.Headers.ContentType.CharSet);
|
||||||
aggregatedResult.ContentType = responseMessage.Content.Headers.ContentType?.MediaType ?? defaultContentType;
|
aggregatedResult.ContentType = responseMessage.Content.Headers.ContentType?.MediaType ?? defaultContentType;
|
||||||
encodingSet = true;
|
encodingSet = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process and rename titles in the content
|
// Process and rename titles in the content
|
||||||
content = _titleMatchingService.RenameTitlesInContent(content, germanTitleVariations, expectedTitle);
|
content = ProcessContent(content, titleMatchVariations, expectedTitle);
|
||||||
|
|
||||||
// Aggregate the items into a single document
|
// Aggregate the items into a single document
|
||||||
aggregatedResult.AggregateItems(content);
|
aggregatedResult.AggregateItems(content);
|
||||||
@@ -129,7 +155,7 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
|
|
||||||
public class SearchController(ProxyService proxyService,
|
public class SearchController(ProxyService proxyService,
|
||||||
TitleMatchingService titleMatchingService,
|
TitleMatchingService titleMatchingService,
|
||||||
TitleQueryService titleQueryService) : SearchControllerBase(proxyService, titleMatchingService)
|
SearchItemLookupService searchItemLookupService) : SearchControllerBase(proxyService, titleMatchingService)
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> MovieSearch([FromRoute] string options, [FromRoute] string domain)
|
public async Task<IActionResult> MovieSearch([FromRoute] string options, [FromRoute] string domain)
|
||||||
@@ -165,54 +191,19 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
q => q.Key,
|
q => q.Key,
|
||||||
q => string.Join(",", q.Value));
|
q => string.Join(",", q.Value));
|
||||||
|
|
||||||
string? searchKey = null;
|
SearchItem? searchItem = null;
|
||||||
string? searchValue = null;
|
string mediaType = "tv";
|
||||||
|
|
||||||
if (queryParameters.TryGetValue("tvdbid", out string? tvdbId))
|
if (queryParameters.TryGetValue("tvdbid", out string? tvdbId) && !string.IsNullOrEmpty(tvdbId))
|
||||||
{
|
{
|
||||||
searchKey = "tvdbid";
|
searchItem = await searchItemLookupService.GetOrFetchSearchItemByExternalId(mediaType, tvdbId);
|
||||||
searchValue = tvdbId;
|
|
||||||
}
|
}
|
||||||
else if (queryParameters.TryGetValue("q", out string? title))
|
else if (queryParameters.TryGetValue("q", out string? title) && !string.IsNullOrEmpty(title))
|
||||||
{
|
{
|
||||||
searchKey = "q";
|
searchItem = await searchItemLookupService.GetOrFetchSearchItemByTitle(mediaType, title);
|
||||||
searchValue = title;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the search if a valid search key was identified
|
return await BaseSearch(options, domain, queryParameters, searchItem);
|
||||||
if (searchKey != null && searchValue != null)
|
|
||||||
{
|
|
||||||
var (hasGermanUmlaut, germanTitle, expectedTitle) = searchKey == "tvdbid"
|
|
||||||
? await titleQueryService.QueryGermanShowTitleByTVDBId(searchValue)
|
|
||||||
: await titleQueryService.QueryGermanShowTitleByTitle(searchValue);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(germanTitle) && !string.IsNullOrEmpty(expectedTitle))
|
|
||||||
{
|
|
||||||
var initialSearchResult = await BaseSearch(options, domain, queryParameters, germanTitle, expectedTitle, hasGermanUmlaut);
|
|
||||||
|
|
||||||
// Additional search with german title because the automatic tvdbid association often fails at the indexer too if there are umlauts
|
|
||||||
if (hasGermanUmlaut && searchKey == "tvdbid")
|
|
||||||
{
|
|
||||||
// Remove identifiers for subsequent searches
|
|
||||||
queryParameters.Remove("tvdbid");
|
|
||||||
queryParameters.Remove("tvmazeid");
|
|
||||||
queryParameters.Remove("imdbid");
|
|
||||||
|
|
||||||
// Aggregate the initial search result with additional results
|
|
||||||
var germanTitleVariations = _titleMatchingService.GenerateTitleVariations(germanTitle);
|
|
||||||
var aggregatedResult = await AggregateSearchResults(domain, queryParameters, germanTitleVariations, expectedTitle);
|
|
||||||
// todo processedContent wie in BaseSearch
|
|
||||||
|
|
||||||
|
|
||||||
aggregatedResult.AggregateItems((initialSearchResult as ContentResult)?.Content ?? "");
|
|
||||||
|
|
||||||
return Content(aggregatedResult.Content, aggregatedResult.ContentType, aggregatedResult.ContentEncoding);
|
|
||||||
}
|
|
||||||
return initialSearchResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await BaseSearch(options, domain, queryParameters);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Text;
|
||||||
using System.Text;
|
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
|
||||||
namespace UmlautAdaptarr.Models
|
namespace UmlautAdaptarr.Models
|
||||||
@@ -9,13 +8,13 @@ namespace UmlautAdaptarr.Models
|
|||||||
public XDocument ContentDocument { get; private set; }
|
public XDocument ContentDocument { get; private set; }
|
||||||
public string ContentType { get; set; }
|
public string ContentType { get; set; }
|
||||||
public Encoding ContentEncoding { get; set; }
|
public Encoding ContentEncoding { get; set; }
|
||||||
private HashSet<string> _uniqueItems;
|
private readonly HashSet<string> _uniqueItems;
|
||||||
|
|
||||||
public AggregatedSearchResult(string contentType, Encoding contentEncoding)
|
public AggregatedSearchResult(string contentType, Encoding contentEncoding)
|
||||||
{
|
{
|
||||||
ContentType = contentType;
|
ContentType = contentType;
|
||||||
ContentEncoding = contentEncoding;
|
ContentEncoding = contentEncoding;
|
||||||
_uniqueItems = new HashSet<string>();
|
_uniqueItems = [];
|
||||||
|
|
||||||
// Initialize ContentDocument with a basic RSS structure
|
// Initialize ContentDocument with a basic RSS structure
|
||||||
ContentDocument = new XDocument(new XElement("rss", new XElement("channel")));
|
ContentDocument = new XDocument(new XElement("rss", new XElement("channel")));
|
||||||
@@ -35,10 +34,6 @@ namespace UmlautAdaptarr.Models
|
|||||||
{
|
{
|
||||||
ContentDocument.Root.Element("channel").Add(item);
|
ContentDocument.Root.Element("channel").Add(item);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
88
UmlautAdaptarr/Models/SearchItem.cs
Normal file
88
UmlautAdaptarr/Models/SearchItem.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using UmlautAdaptarr.Utilities;
|
||||||
|
|
||||||
|
namespace UmlautAdaptarr.Models
|
||||||
|
{
|
||||||
|
public partial class SearchItem
|
||||||
|
{
|
||||||
|
public int ArrId { get; set; }
|
||||||
|
public string ExternalId { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public bool HasGermanUmlaut => Title?.HasGermanUmlauts() ?? false;
|
||||||
|
public string ExpectedTitle { get; set; }
|
||||||
|
public string? GermanTitle { get; set; }
|
||||||
|
public string[] TitleSearchVariations { get; set; }
|
||||||
|
public string[] TitleMatchVariations { get; set; }
|
||||||
|
public string MediaType { get; set; }
|
||||||
|
// TODO public MediaType instead of string
|
||||||
|
|
||||||
|
public SearchItem(int arrId, string externalId, string title, string expectedTitle, string? germanTitle, string mediaType, string[]? aliases)
|
||||||
|
{
|
||||||
|
ArrId = arrId;
|
||||||
|
ExternalId = externalId;
|
||||||
|
Title = title;
|
||||||
|
ExpectedTitle = expectedTitle;
|
||||||
|
GermanTitle = germanTitle;
|
||||||
|
TitleSearchVariations = GenerateTitleVariations(germanTitle).ToArray();
|
||||||
|
MediaType = mediaType;
|
||||||
|
|
||||||
|
var allTitleVariations = new List<string>(TitleSearchVariations);
|
||||||
|
|
||||||
|
// If aliases are not null, generate variations for each and add them to the list
|
||||||
|
// TODO (not necessarily here) only use deu and eng alias
|
||||||
|
if (aliases != null)
|
||||||
|
{
|
||||||
|
foreach (var alias in aliases)
|
||||||
|
{
|
||||||
|
allTitleVariations.AddRange(GenerateTitleVariations(alias));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TitleMatchVariations = allTitleVariations.Distinct().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<string> GenerateTitleVariations(string? germanTitle)
|
||||||
|
{
|
||||||
|
if (germanTitle == null)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
var cleanTitle = germanTitle.RemoveAccentButKeepGermanUmlauts();
|
||||||
|
|
||||||
|
// Start with base variations including handling umlauts
|
||||||
|
var baseVariations = new List<string>
|
||||||
|
{
|
||||||
|
cleanTitle, // No change
|
||||||
|
cleanTitle.ReplaceGermanUmlautsWithLatinEquivalents(),
|
||||||
|
cleanTitle.RemoveGermanUmlautDots()
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: determine if this is really needed
|
||||||
|
// Additional variations to accommodate titles with "-"
|
||||||
|
if (cleanTitle.Contains('-'))
|
||||||
|
{
|
||||||
|
var withoutDash = cleanTitle.Replace("-", "");
|
||||||
|
var withSpaceInsteadOfDash = cleanTitle.Replace("-", " ");
|
||||||
|
|
||||||
|
// Add variations of the title without dash and with space instead of dash
|
||||||
|
baseVariations.AddRange(new List<string>
|
||||||
|
{
|
||||||
|
withoutDash,
|
||||||
|
withSpaceInsteadOfDash,
|
||||||
|
withoutDash.ReplaceGermanUmlautsWithLatinEquivalents(),
|
||||||
|
withoutDash.RemoveGermanUmlautDots(),
|
||||||
|
withSpaceInsteadOfDash.ReplaceGermanUmlautsWithLatinEquivalents(),
|
||||||
|
withSpaceInsteadOfDash.RemoveGermanUmlautDots()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove multiple spaces
|
||||||
|
var cleanedVariations = baseVariations.Select(variation => MultipleWhitespaceRegex().Replace(variation, " "));
|
||||||
|
|
||||||
|
return cleanedVariations.Distinct();
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\s+")]
|
||||||
|
private static partial Regex MultipleWhitespaceRegex();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using UmlautAdaptarr.Providers;
|
||||||
using UmlautAdaptarr.Routing;
|
using UmlautAdaptarr.Routing;
|
||||||
using UmlautAdaptarr.Services;
|
using UmlautAdaptarr.Services;
|
||||||
|
|
||||||
@@ -29,14 +30,17 @@ internal class Program
|
|||||||
|
|
||||||
builder.Services.AddMemoryCache(options =>
|
builder.Services.AddMemoryCache(options =>
|
||||||
{
|
{
|
||||||
options.SizeLimit = 500;
|
//options.SizeLimit = 20000;
|
||||||
});
|
});
|
||||||
builder.Services.AddSingleton<ILogger, Logger<TitleQueryService>>();
|
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddScoped<TitleQueryService>();
|
builder.Services.AddHostedService<ArrSyncBackgroundService>();
|
||||||
builder.Services.AddScoped<ProxyService>();
|
builder.Services.AddSingleton<TitleApiService>(); // TODO rename
|
||||||
builder.Services.AddScoped<TitleMatchingService>();
|
builder.Services.AddSingleton<SearchItemLookupService>();
|
||||||
|
builder.Services.AddSingleton<TitleMatchingService>();
|
||||||
|
builder.Services.AddSingleton<SonarrClient>();
|
||||||
|
builder.Services.AddSingleton<CacheService>();
|
||||||
|
builder.Services.AddSingleton<ProxyService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,10 @@
|
|||||||
"profiles": {
|
"profiles": {
|
||||||
"http": {
|
"http": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"launchBrowser": true,
|
|
||||||
"_launchUrl": "optionsTODO/example.com/api?t=movie&apikey=132&imdbid=123&limit=100",
|
|
||||||
"launchUrl": "/",
|
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
|
"_launchUrl": "optionsTODO/example.com/api?t=movie&apikey=132&imdbid=123&limit=100",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"applicationUrl": "http://localhost:5182"
|
"applicationUrl": "http://localhost:5182"
|
||||||
},
|
},
|
||||||
|
|||||||
13
UmlautAdaptarr/Providers/ArrClientBase.cs
Normal file
13
UmlautAdaptarr/Providers/ArrClientBase.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using UmlautAdaptarr.Models;
|
||||||
|
using UmlautAdaptarr.Services;
|
||||||
|
|
||||||
|
namespace UmlautAdaptarr.Providers
|
||||||
|
{
|
||||||
|
public abstract class ArrClientBase()
|
||||||
|
{
|
||||||
|
public abstract Task<IEnumerable<SearchItem>> FetchAllItemsAsync();
|
||||||
|
public abstract Task<SearchItem?> FetchItemByExternalIdAsync(string externalId);
|
||||||
|
public abstract Task<SearchItem?> FetchItemByTitleAsync(string title);
|
||||||
|
}
|
||||||
|
}
|
||||||
172
UmlautAdaptarr/Providers/SonarrClient.cs
Normal file
172
UmlautAdaptarr/Providers/SonarrClient.cs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Net.Http;
|
||||||
|
using UmlautAdaptarr.Models;
|
||||||
|
using UmlautAdaptarr.Services;
|
||||||
|
using UmlautAdaptarr.Utilities;
|
||||||
|
|
||||||
|
namespace UmlautAdaptarr.Providers
|
||||||
|
{
|
||||||
|
public class SonarrClient(
|
||||||
|
IHttpClientFactory clientFactory,
|
||||||
|
IConfiguration configuration,
|
||||||
|
TitleApiService titleService,
|
||||||
|
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");
|
||||||
|
private readonly string _mediaType = "tv";
|
||||||
|
|
||||||
|
public override async Task<IEnumerable<SearchItem>> FetchAllItemsAsync()
|
||||||
|
{
|
||||||
|
var httpClient = clientFactory.CreateClient();
|
||||||
|
var items = new List<SearchItem>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
var sonarrUrl = $"{_sonarrHost}/api/v3/series?includeSeasonImages=false&apikey={_sonarrApiKey}";
|
||||||
|
logger.LogInformation($"Fetching all items from Sonarr: {UrlUtilities.RedactApiKey(sonarrUrl)}");
|
||||||
|
var response = await httpClient.GetStringAsync(sonarrUrl);
|
||||||
|
var shows = JsonConvert.DeserializeObject<List<dynamic>>(response);
|
||||||
|
|
||||||
|
if (shows != null)
|
||||||
|
{
|
||||||
|
logger.LogInformation($"Successfully fetched {shows.Count} items from Sonarr.");
|
||||||
|
foreach (var show in shows)
|
||||||
|
{
|
||||||
|
var tvdbId = (string)show.tvdbId;
|
||||||
|
if (tvdbId == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning($"Sonarr Show {show.id} doesn't have a tvdbId.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
(var germanTitle, var aliases) = await titleService.FetchGermanTitleAndAliasesByExternalIdAsync(_mediaType, tvdbId);
|
||||||
|
var searchItem = new SearchItem
|
||||||
|
(
|
||||||
|
arrId: (int)show.id,
|
||||||
|
externalId: tvdbId,
|
||||||
|
title: (string)show.title,
|
||||||
|
expectedTitle: (string)show.title,
|
||||||
|
germanTitle: germanTitle,
|
||||||
|
aliases: aliases,
|
||||||
|
mediaType: _mediaType
|
||||||
|
);
|
||||||
|
|
||||||
|
items.Add(searchItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation($"Finished fetching all items from Sonarr");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError($"Error fetching all shows from Sonarr: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<SearchItem?> FetchItemByExternalIdAsync(string externalId)
|
||||||
|
{
|
||||||
|
var httpClient = clientFactory.CreateClient();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sonarrUrl = $"{_sonarrHost}/api/v3/series?tvdbId={externalId}&includeSeasonImages=false&apikey={_sonarrApiKey}";
|
||||||
|
logger.LogInformation($"Fetching item by external ID from Sonarr: {UrlUtilities.RedactApiKey(sonarrUrl)}");
|
||||||
|
var response = await httpClient.GetStringAsync(sonarrUrl);
|
||||||
|
var shows = JsonConvert.DeserializeObject<dynamic>(response);
|
||||||
|
var show = shows?[0];
|
||||||
|
|
||||||
|
if (show != null)
|
||||||
|
{
|
||||||
|
var tvdbId = (string)show.tvdbId;
|
||||||
|
if (tvdbId == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning($"Sonarr Show {show.id} doesn't have a tvdbId.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
(var germanTitle, var aliases) = await titleService.FetchGermanTitleAndAliasesByExternalIdAsync(_mediaType, tvdbId);
|
||||||
|
|
||||||
|
var searchItem = new SearchItem
|
||||||
|
(
|
||||||
|
arrId: (int)show.id,
|
||||||
|
externalId: tvdbId,
|
||||||
|
title: (string)show.title,
|
||||||
|
expectedTitle: (string)show.title,
|
||||||
|
germanTitle: germanTitle,
|
||||||
|
aliases: aliases,
|
||||||
|
mediaType: _mediaType
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.LogInformation($"Successfully fetched show {searchItem} from Sonarr.");
|
||||||
|
return searchItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError($"Error fetching single show from Sonarr: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<SearchItem?> FetchItemByTitleAsync(string title)
|
||||||
|
{
|
||||||
|
var httpClient = clientFactory.CreateClient();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(string? germanTitle, string? tvdbId, string[]? aliases) = await titleService.FetchGermanTitleAndExternalIdAndAliasesByTitle(_mediaType, title);
|
||||||
|
|
||||||
|
if (tvdbId == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 null;
|
||||||
|
}
|
||||||
|
else if (shows.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning($"No results found for TVDB ID {tvdbId}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedTitle = (string)shows[0].title;
|
||||||
|
if (expectedTitle == null)
|
||||||
|
{
|
||||||
|
logger.LogError($"Sonarr Title for TVDB ID {tvdbId} is null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchItem = new SearchItem
|
||||||
|
(
|
||||||
|
arrId: (int)shows[0].id,
|
||||||
|
externalId: tvdbId,
|
||||||
|
title: (string)shows[0].title,
|
||||||
|
expectedTitle: (string)shows[0].title,
|
||||||
|
germanTitle: germanTitle,
|
||||||
|
aliases: aliases,
|
||||||
|
mediaType: _mediaType
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.LogInformation($"Successfully fetched show {searchItem} from Sonarr.");
|
||||||
|
return searchItem;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError($"Error fetching single show from Sonarr: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
UmlautAdaptarr/Services/ArrSyncBackgroundService.cs
Normal file
75
UmlautAdaptarr/Services/ArrSyncBackgroundService.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UmlautAdaptarr.Models;
|
||||||
|
using UmlautAdaptarr.Providers;
|
||||||
|
|
||||||
|
namespace UmlautAdaptarr.Services
|
||||||
|
{
|
||||||
|
public class ArrSyncBackgroundService(
|
||||||
|
SonarrClient sonarrClient,
|
||||||
|
CacheService cacheService,
|
||||||
|
ILogger<ArrSyncBackgroundService> logger) : BackgroundService
|
||||||
|
{
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
logger.LogInformation("ArrSyncBackgroundService is starting.");
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
logger.LogInformation("ArrSyncBackgroundService is running.");
|
||||||
|
await FetchAndUpdateDataAsync();
|
||||||
|
logger.LogInformation("ArrSyncBackgroundService has completed an iteration.");
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromHours(12), stoppingToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("ArrSyncBackgroundService is stopping.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FetchAndUpdateDataAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await FetchItemsFromSonarrAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "An error occurred while fetching items from the Arrs.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FetchItemsFromSonarrAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var items = await sonarrClient.FetchAllItemsAsync();
|
||||||
|
UpdateSearchItems(items);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "An error occurred while updating search item from Sonarr.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSearchItems(IEnumerable<SearchItem> searchItems)
|
||||||
|
{
|
||||||
|
foreach (var searchItem in searchItems)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cacheService.CacheSearchItem(searchItem);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, $"An error occurred while caching search item with ID {searchItem.ArrId}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
UmlautAdaptarr/Services/CacheService.cs
Normal file
95
UmlautAdaptarr/Services/CacheService.cs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using UmlautAdaptarr.Models;
|
||||||
|
using UmlautAdaptarr.Utilities;
|
||||||
|
|
||||||
|
namespace UmlautAdaptarr.Services
|
||||||
|
{
|
||||||
|
public class CacheService(IMemoryCache cache)
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, HashSet<string>> VariationIndex = [];
|
||||||
|
private const int VARIATION_LOOKUP_CACHE_LENGTH = 5;
|
||||||
|
|
||||||
|
public void CacheSearchItem(SearchItem item)
|
||||||
|
{
|
||||||
|
var prefix = item.MediaType;
|
||||||
|
var normalizedTitle = item.Title.RemoveAccentButKeepGermanUmlauts().ToLower();
|
||||||
|
// TODO maybe we need to also add the media type (movie/book/show etc)
|
||||||
|
|
||||||
|
cache.Set($"{prefix}_extid_{item.ExternalId}", item);
|
||||||
|
cache.Set($"{prefix}_title_{normalizedTitle}", item);
|
||||||
|
|
||||||
|
foreach (var variation in item.TitleSearchVariations)
|
||||||
|
{
|
||||||
|
var normalizedVariation = item.Title.RemoveAccentButKeepGermanUmlauts().ToLower();
|
||||||
|
var cacheKey = $"{prefix}_var_{normalizedVariation}";
|
||||||
|
cache.Set(cacheKey, item);
|
||||||
|
|
||||||
|
// Indexing by prefix
|
||||||
|
var indexPrefix = normalizedVariation[..Math.Min(VARIATION_LOOKUP_CACHE_LENGTH, variation.Length)].ToLower();
|
||||||
|
if (!VariationIndex.ContainsKey(indexPrefix))
|
||||||
|
{
|
||||||
|
VariationIndex[indexPrefix] = new HashSet<string>();
|
||||||
|
}
|
||||||
|
VariationIndex[indexPrefix].Add(cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchItem? SearchItemByTitle(string mediaType, string title)
|
||||||
|
{
|
||||||
|
var normalizedTitle = title.RemoveAccentButKeepGermanUmlauts().ToLower();
|
||||||
|
|
||||||
|
// Use the first few characters of the normalized title for cache prefix search
|
||||||
|
var cacheSearchPrefix = normalizedTitle[..Math.Min(VARIATION_LOOKUP_CACHE_LENGTH, normalizedTitle.Length)];
|
||||||
|
|
||||||
|
if (VariationIndex.TryGetValue(cacheSearchPrefix, out var cacheKeys))
|
||||||
|
{
|
||||||
|
foreach (var cacheKey in cacheKeys)
|
||||||
|
{
|
||||||
|
if (cache.TryGetValue(cacheKey, out SearchItem? item))
|
||||||
|
{
|
||||||
|
if (item?.MediaType != mediaType)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// After finding a potential item, compare normalizedTitle with each German title variation
|
||||||
|
foreach (var variation in item?.TitleSearchVariations ?? [])
|
||||||
|
{
|
||||||
|
var normalizedVariation = variation.RemoveAccentButKeepGermanUmlauts().ToLower();
|
||||||
|
if (normalizedTitle.StartsWith(variation, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchItem? GetSearchItemByExternalId(string mediaType, string externalId)
|
||||||
|
{
|
||||||
|
if (cache.TryGetValue($"{mediaType}_extid_{externalId}", out SearchItem? item))
|
||||||
|
{
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchItem? GetSearchItemByTitle(string mediaType, string title)
|
||||||
|
{
|
||||||
|
var normalizedTitle = title.RemoveAccentButKeepGermanUmlauts().ToLower();
|
||||||
|
|
||||||
|
if (mediaType == "generic")
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
cache.TryGetValue($"{mediaType}_var_{normalizedTitle}", out SearchItem? item);
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
cache.TryGetValue($"{mediaType}_title_{normalizedTitle}", out item);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,58 @@
|
|||||||
namespace UmlautAdaptarr.Services
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using UmlautAdaptarr.Utilities;
|
||||||
|
|
||||||
|
namespace UmlautAdaptarr.Services
|
||||||
{
|
{
|
||||||
public class ProxyService(IHttpClientFactory clientFactory, IConfiguration configuration)
|
public class ProxyService
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient = clientFactory.CreateClient("HttpClient") ?? throw new ArgumentNullException();
|
private readonly HttpClient _httpClient;
|
||||||
private readonly string _userAgent = configuration["Settings:UserAgent"] ?? throw new ArgumentException("UserAgent must be set in appsettings.json");
|
private readonly string _userAgent;
|
||||||
// TODO: Add cache!
|
private readonly ILogger<ProxyService> _logger;
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private static readonly ConcurrentDictionary<string, DateTimeOffset> _lastRequestTimes = new();
|
||||||
|
|
||||||
|
public ProxyService(IHttpClientFactory clientFactory, IConfiguration configuration, ILogger<ProxyService> logger, IMemoryCache cache)
|
||||||
|
{
|
||||||
|
_httpClient = clientFactory.CreateClient("HttpClient") ?? throw new ArgumentNullException(nameof(clientFactory));
|
||||||
|
_userAgent = configuration["Settings:UserAgent"] ?? throw new ArgumentException("UserAgent must be set in appsettings.json");
|
||||||
|
_logger = logger;
|
||||||
|
_cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> ProxyRequestAsync(HttpContext context, string targetUri)
|
public async Task<HttpResponseMessage> ProxyRequestAsync(HttpContext context, string targetUri)
|
||||||
{
|
{
|
||||||
var requestMessage = new HttpRequestMessage();
|
if (!HttpMethods.IsGet(context.Request.Method))
|
||||||
var requestMethod = context.Request.Method;
|
|
||||||
|
|
||||||
if (!HttpMethods.IsGet(requestMethod))
|
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Only GET requests are supported", nameof(requestMethod));
|
throw new ArgumentException("Only GET requests are supported", context.Request.Method);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the request headers
|
// Throttling mechanism
|
||||||
|
var host = new Uri(targetUri).Host;
|
||||||
|
if (_lastRequestTimes.TryGetValue(host, out var lastRequestTime))
|
||||||
|
{
|
||||||
|
var timeSinceLastRequest = DateTimeOffset.Now - lastRequestTime;
|
||||||
|
if (timeSinceLastRequest < TimeSpan.FromSeconds(3))
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(3) - timeSinceLastRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_lastRequestTimes[host] = DateTimeOffset.Now;
|
||||||
|
|
||||||
|
// Check cache
|
||||||
|
if (_cache.TryGetValue(targetUri, out HttpResponseMessage cachedResponse))
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"Returning cached response for {UrlUtilities.RedactApiKey(targetUri)}");
|
||||||
|
return cachedResponse!;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestMessage = new HttpRequestMessage
|
||||||
|
{
|
||||||
|
RequestUri = new Uri(targetUri),
|
||||||
|
Method = HttpMethod.Get,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy request headers
|
||||||
foreach (var header in context.Request.Headers)
|
foreach (var header in context.Request.Headers)
|
||||||
{
|
{
|
||||||
if (header.Key == "User-Agent" && _userAgent.Length != 0)
|
if (header.Key == "User-Agent" && _userAgent.Length != 0)
|
||||||
@@ -29,34 +65,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestMessage.RequestUri = new Uri(targetUri);
|
|
||||||
requestMessage.Method = HttpMethod.Get;
|
|
||||||
|
|
||||||
//var responseMessage = await _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var responseMessage = _httpClient.Send(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
|
_logger.LogInformation($"ProxyService GET {UrlUtilities.RedactApiKey(targetUri)}");
|
||||||
|
var responseMessage = await _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
|
||||||
|
|
||||||
// TODO: Handle 503 etc
|
if (responseMessage.IsSuccessStatusCode)
|
||||||
responseMessage.EnsureSuccessStatusCode();
|
{
|
||||||
|
_cache.Set(targetUri, responseMessage, TimeSpan.FromMinutes(5));
|
||||||
// Modify the response content if necessary
|
}
|
||||||
/*var content = await responseMessage.Content.ReadAsStringAsync();
|
|
||||||
content = ReplaceCharacters(content);
|
|
||||||
responseMessage.Content = new StringContent(content);*/
|
|
||||||
|
|
||||||
return responseMessage;
|
return responseMessage;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine(ex.ToString());
|
_logger.LogError(ex, $"Error proxying request: {UrlUtilities.RedactApiKey(targetUri)}. Error: {ex.Message}");
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ReplaceCharacters(string input)
|
// Create a response message indicating an internal server error
|
||||||
|
var errorResponse = new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError)
|
||||||
{
|
{
|
||||||
return input.Replace("Ä", "AE");
|
Content = new StringContent($"An error occurred while processing your request: {ex.Message}")
|
||||||
|
};
|
||||||
|
return errorResponse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
UmlautAdaptarr/Services/SearchItemLookupService.cs
Normal file
65
UmlautAdaptarr/Services/SearchItemLookupService.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using UmlautAdaptarr.Models;
|
||||||
|
using UmlautAdaptarr.Providers;
|
||||||
|
|
||||||
|
namespace UmlautAdaptarr.Services
|
||||||
|
{
|
||||||
|
public class SearchItemLookupService(CacheService cacheService, SonarrClient sonarrClient)
|
||||||
|
{
|
||||||
|
public async Task<SearchItem?> GetOrFetchSearchItemByExternalId(string mediaType, string externalId)
|
||||||
|
{
|
||||||
|
// Attempt to get the item from the cache first
|
||||||
|
var cachedItem = cacheService.GetSearchItemByExternalId(mediaType, externalId);
|
||||||
|
if (cachedItem != null)
|
||||||
|
{
|
||||||
|
return cachedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found in cache, fetch from the appropriate source
|
||||||
|
SearchItem? fetchedItem = null;
|
||||||
|
switch (mediaType)
|
||||||
|
{
|
||||||
|
case "tv":
|
||||||
|
fetchedItem = await sonarrClient.FetchItemByExternalIdAsync(externalId);
|
||||||
|
break;
|
||||||
|
// TODO Add cases for other sources like Radarr, Lidarr, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an item is fetched, cache it
|
||||||
|
if (fetchedItem != null)
|
||||||
|
{
|
||||||
|
cacheService.CacheSearchItem(fetchedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SearchItem?> GetOrFetchSearchItemByTitle(string mediaType, string title)
|
||||||
|
{
|
||||||
|
// Attempt to get the item from the cache first
|
||||||
|
var cachedItem = cacheService.GetSearchItemByTitle(mediaType, title);
|
||||||
|
if (cachedItem != null)
|
||||||
|
{
|
||||||
|
return cachedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found in cache, fetch from the appropriate source
|
||||||
|
SearchItem? fetchedItem = null;
|
||||||
|
switch (mediaType)
|
||||||
|
{
|
||||||
|
case "tv":
|
||||||
|
fetchedItem = await sonarrClient.FetchItemByTitleAsync(title);
|
||||||
|
break;
|
||||||
|
// TODO add cases for other sources as needed, such as Radarr, Lidarr, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an item is fetched, cache it
|
||||||
|
if (fetchedItem != null)
|
||||||
|
{
|
||||||
|
cacheService.CacheSearchItem(fetchedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchedItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
98
UmlautAdaptarr/Services/TitleApiService.cs
Normal file
98
UmlautAdaptarr/Services/TitleApiService.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace UmlautAdaptarr.Services
|
||||||
|
{
|
||||||
|
public class TitleApiService(IHttpClientFactory clientFactory, IConfiguration configuration, ILogger<TitleApiService> logger)
|
||||||
|
{
|
||||||
|
private readonly string _umlautAdaptarrApiHost = configuration["Settings:UmlautAdaptarrApiHost"]
|
||||||
|
?? throw new ArgumentException("UmlautAdaptarrApiHost must be set in appsettings.json");
|
||||||
|
private DateTime lastRequestTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private async Task EnsureMinimumDelayAsync()
|
||||||
|
{
|
||||||
|
var sinceLastRequest = DateTime.Now - lastRequestTime;
|
||||||
|
if (sinceLastRequest < TimeSpan.FromSeconds(2))
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(2) - sinceLastRequest);
|
||||||
|
}
|
||||||
|
lastRequestTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string? germanTitle, string[]? aliases)> FetchGermanTitleAndAliasesByExternalIdAsync(string mediaType, string externalId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await EnsureMinimumDelayAsync();
|
||||||
|
|
||||||
|
var httpClient = clientFactory.CreateClient();
|
||||||
|
var titleApiUrl = $"{_umlautAdaptarrApiHost}/tvshow_german.php?tvdbid={externalId}";
|
||||||
|
var response = await httpClient.GetStringAsync(titleApiUrl);
|
||||||
|
var titleApiResponseData = JsonConvert.DeserializeObject<dynamic>(response);
|
||||||
|
|
||||||
|
if (titleApiResponseData == null)
|
||||||
|
{
|
||||||
|
logger.LogError($"Parsing UmlautAdaptarr TitleQuery API response for mediaType {mediaType} with external id {externalId} resulted in null");
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (titleApiResponseData.status == "success" && !string.IsNullOrEmpty((string)titleApiResponseData.germanTitle))
|
||||||
|
{
|
||||||
|
// TODO add filter for german aliases only in API
|
||||||
|
// then also add if there is a "deu" alias to search for it via text
|
||||||
|
string[]? aliases = null;
|
||||||
|
|
||||||
|
if (titleApiResponseData.aliases != null)
|
||||||
|
{
|
||||||
|
// Parse the aliases as a JArray
|
||||||
|
JArray aliasesArray = JArray.FromObject(titleApiResponseData.aliases);
|
||||||
|
|
||||||
|
// Project the 'name' field from each object in the array
|
||||||
|
aliases = aliasesArray.Children<JObject>()
|
||||||
|
.Select(alias => alias["name"].ToString())
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
return (titleApiResponseData.germanTitle, aliases);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError($"Error fetching German title for TVDB ID {externalId}: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string? germanTitle, string? externalId, string[]? aliases)> FetchGermanTitleAndExternalIdAndAliasesByTitle(string mediaType, string title)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await EnsureMinimumDelayAsync();
|
||||||
|
|
||||||
|
var httpClient = clientFactory.CreateClient();
|
||||||
|
var tvdbCleanTitle = title.Replace("ß", "ss");
|
||||||
|
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 (null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (titleApiResponseData.status == "success" && !string.IsNullOrEmpty((string)titleApiResponseData.germanTitle))
|
||||||
|
{
|
||||||
|
string[] aliases = titleApiResponseData.aliases.ToObject<string[]>();
|
||||||
|
return (titleApiResponseData.germanTitle, titleApiResponseData.tvdbId, aliases);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError($"Error fetching German title for {mediaType} with title {title}: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,46 +4,15 @@ using UmlautAdaptarr.Utilities;
|
|||||||
|
|
||||||
namespace UmlautAdaptarr.Services
|
namespace UmlautAdaptarr.Services
|
||||||
{
|
{
|
||||||
public partial class TitleMatchingService
|
public partial class TitleMatchingService(CacheService cacheService, ILogger<TitleMatchingService> logger)
|
||||||
{
|
{
|
||||||
public List<string> GenerateTitleVariations(string germanTitle)
|
public string RenameTitlesInContent(string content, string[]? titleMatchVariations, string? expectedTitle)
|
||||||
{
|
|
||||||
var cleanTitle = germanTitle.RemoveAccentButKeepGermanUmlauts();
|
|
||||||
|
|
||||||
// Start with base variations including handling umlauts
|
|
||||||
var baseVariations = new List<string>
|
|
||||||
{
|
|
||||||
cleanTitle, // No change
|
|
||||||
cleanTitle.ReplaceGermanUmlautsWithLatinEquivalents(),
|
|
||||||
cleanTitle.RemoveGermanUmlautDots()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Additional variations to accommodate titles with "-"
|
|
||||||
if (cleanTitle.Contains('-'))
|
|
||||||
{
|
|
||||||
var withoutDash = cleanTitle.Replace("-", "");
|
|
||||||
var withSpaceInsteadOfDash = cleanTitle.Replace("-", " ");
|
|
||||||
|
|
||||||
// Add variations of the title without dash and with space instead of dash
|
|
||||||
baseVariations.AddRange(new List<string>
|
|
||||||
{
|
|
||||||
withoutDash,
|
|
||||||
withSpaceInsteadOfDash,
|
|
||||||
withoutDash.ReplaceGermanUmlautsWithLatinEquivalents(),
|
|
||||||
withoutDash.RemoveGermanUmlautDots(),
|
|
||||||
withSpaceInsteadOfDash.ReplaceGermanUmlautsWithLatinEquivalents(),
|
|
||||||
withSpaceInsteadOfDash.RemoveGermanUmlautDots()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseVariations.Distinct().ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public string RenameTitlesInContent(string content, List<string> germanTitleVariations, string expectedTitle)
|
|
||||||
{
|
{
|
||||||
var xDoc = XDocument.Parse(content);
|
var xDoc = XDocument.Parse(content);
|
||||||
|
|
||||||
|
// If expectedTitle and titleMatchVariations are provided use them, if not use the CacheService to find matches.
|
||||||
|
bool useCacheService = string.IsNullOrEmpty(expectedTitle) || titleMatchVariations?.Length == 0;
|
||||||
|
|
||||||
foreach (var item in xDoc.Descendants("item"))
|
foreach (var item in xDoc.Descendants("item"))
|
||||||
{
|
{
|
||||||
var titleElement = item.Element("title");
|
var titleElement = item.Element("title");
|
||||||
@@ -52,9 +21,40 @@ namespace UmlautAdaptarr.Services
|
|||||||
var originalTitle = titleElement.Value;
|
var originalTitle = titleElement.Value;
|
||||||
var normalizedOriginalTitle = NormalizeTitle(originalTitle);
|
var normalizedOriginalTitle = NormalizeTitle(originalTitle);
|
||||||
|
|
||||||
// Attempt to find a variation that matches the start of the original title
|
if (useCacheService)
|
||||||
foreach (var variation in germanTitleVariations)
|
|
||||||
{
|
{
|
||||||
|
var categoryElement = item.Element("category");
|
||||||
|
var category = categoryElement?.Value;
|
||||||
|
var mediaType = GetMediaTypeFromCategory(category);
|
||||||
|
if (mediaType == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use CacheService to find a matching SearchItem by title
|
||||||
|
var searchItem = cacheService.SearchItemByTitle(mediaType, originalTitle);
|
||||||
|
if (searchItem != null)
|
||||||
|
{
|
||||||
|
// If a SearchItem is found, use its ExpectedTitle and titleMatchVariations for renaming
|
||||||
|
expectedTitle = searchItem.ExpectedTitle;
|
||||||
|
titleMatchVariations = searchItem.TitleMatchVariations;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Skip processing this item if no matching SearchItem is found
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to find a variation that matches the start of the original title
|
||||||
|
foreach (var variation in titleMatchVariations!)
|
||||||
|
{
|
||||||
|
// Skip variations that are already the expectedTitle
|
||||||
|
if (variation == expectedTitle)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Variation is already normalized at creation
|
// Variation is already normalized at creation
|
||||||
var pattern = "^" + Regex.Escape(variation).Replace("\\ ", "[._ ]");
|
var pattern = "^" + Regex.Escape(variation).Replace("\\ ", "[._ ]");
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ namespace UmlautAdaptarr.Services
|
|||||||
// Find the first separator used in the original title for consistent replacement
|
// Find the first separator used in the original title for consistent replacement
|
||||||
var separator = FindFirstSeparator(originalTitle);
|
var separator = FindFirstSeparator(originalTitle);
|
||||||
// Reconstruct the expected title using the original separator
|
// Reconstruct the expected title using the original separator
|
||||||
var newTitlePrefix = expectedTitle.Replace(" ", separator.ToString());
|
var newTitlePrefix = expectedTitle!.Replace(" ", separator.ToString());
|
||||||
|
|
||||||
// Extract the suffix from the original title starting right after the matched variation length
|
// Extract the suffix from the original title starting right after the matched variation length
|
||||||
var variationLength = variation.Length;
|
var variationLength = variation.Length;
|
||||||
@@ -88,7 +88,10 @@ namespace UmlautAdaptarr.Services
|
|||||||
var newTitle = newTitlePrefix + (string.IsNullOrEmpty(suffix) ? "" : separator + suffix);
|
var newTitle = newTitlePrefix + (string.IsNullOrEmpty(suffix) ? "" : separator + suffix);
|
||||||
|
|
||||||
// Update the title element's value with the new title
|
// Update the title element's value with the new title
|
||||||
titleElement.Value = newTitle + $"({originalTitle.Substring(0, variationLength)})";
|
//titleElement.Value = newTitle + $"({originalTitle.Substring(0, variationLength)})";
|
||||||
|
titleElement.Value = newTitle;
|
||||||
|
|
||||||
|
logger.LogInformation($"TitleMatchingService - Title changed: '{originalTitle}' to '{newTitle}'");
|
||||||
break; // Break after the first successful match and modification
|
break; // Break after the first successful match and modification
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +105,7 @@ namespace UmlautAdaptarr.Services
|
|||||||
private static string NormalizeTitle(string title)
|
private static string NormalizeTitle(string title)
|
||||||
{
|
{
|
||||||
title = title.RemoveAccentButKeepGermanUmlauts();
|
title = title.RemoveAccentButKeepGermanUmlauts();
|
||||||
// Replace all known separators with a consistent one for normalization
|
// Replace all known separators with space for normalization
|
||||||
return WordSeperationCharRegex().Replace(title, " ".ToString());
|
return WordSeperationCharRegex().Replace(title, " ".ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +121,33 @@ namespace UmlautAdaptarr.Services
|
|||||||
return title.Replace(' ', separator);
|
return title.Replace(' ', separator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string? GetMediaTypeFromCategory(string? category)
|
||||||
|
{
|
||||||
|
if (category == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category.StartsWith("EBook", StringComparison.OrdinalIgnoreCase) || category.StartsWith("Book", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "book";
|
||||||
|
}
|
||||||
|
else if (category.StartsWith("Movies", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "movies";
|
||||||
|
}
|
||||||
|
else if (category.StartsWith("TV", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "tv";
|
||||||
|
}
|
||||||
|
else if (category.Contains("Audiobook", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "book";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[GeneratedRegex("[._ ]")]
|
[GeneratedRegex("[._ ]")]
|
||||||
private static partial Regex WordSeperationCharRegex();
|
private static partial Regex WordSeperationCharRegex();
|
||||||
|
|||||||
@@ -1,187 +0,0 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using UmlautAdaptarr.Utilities;
|
|
||||||
|
|
||||||
namespace UmlautAdaptarr.Services
|
|
||||||
{
|
|
||||||
public class TitleQueryService
|
|
||||||
{
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
private readonly IMemoryCache _cache;
|
|
||||||
private readonly ILogger<TitleQueryService> _logger;
|
|
||||||
private readonly string _sonarrHost;
|
|
||||||
private readonly string _sonarrApiKey;
|
|
||||||
private readonly string _umlautAdaptarrApiHost;
|
|
||||||
|
|
||||||
public TitleQueryService(IMemoryCache memoryCache, ILogger<TitleQueryService> logger, IConfiguration configuration, IHttpClientFactory clientFactory)
|
|
||||||
{
|
|
||||||
_httpClient = clientFactory.CreateClient("HttpClient") ?? throw new ArgumentNullException();
|
|
||||||
_cache = memoryCache;
|
|
||||||
_logger = logger;
|
|
||||||
|
|
||||||
_sonarrHost = configuration.GetValue<string>("SONARR_HOST");
|
|
||||||
_sonarrApiKey = configuration.GetValue<string>("SONARR_API_KEY");
|
|
||||||
_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 cacheKey = $"show_{tvdbId}";
|
|
||||||
if (_cache.TryGetValue(cacheKey, out (bool hasGermanUmlaut, string? GermanTitle, string ExpectedTitle) cachedResult))
|
|
||||||
{
|
|
||||||
return cachedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sonarrUrl = $"{_sonarrHost}/api/v3/series?tvdbId={tvdbId}&includeSeasonImages=false&apikey={_sonarrApiKey}";
|
|
||||||
var response = await _httpClient.GetStringAsync(sonarrUrl);
|
|
||||||
var shows = JsonConvert.DeserializeObject<dynamic>(response);
|
|
||||||
|
|
||||||
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 originalLanguage = (string)shows[0].originalLanguage.name;
|
|
||||||
|
|
||||||
if (originalLanguage != "German")
|
|
||||||
{
|
|
||||||
var apiUrl = $"{_umlautAdaptarrApiHost}/tvshow_german.php?tvdbid={tvdbId}";
|
|
||||||
var apiResponse = await _httpClient.GetStringAsync(apiUrl);
|
|
||||||
var responseData = JsonConvert.DeserializeObject<dynamic>(apiResponse);
|
|
||||||
|
|
||||||
if (responseData == null)
|
|
||||||
{
|
|
||||||
_logger.LogError($"Parsing UmlautAdaptarr TitleQuery API response for TVDB ID {tvdbId} resulted in null");
|
|
||||||
return (false, null, string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (responseData.status == "success" && !string.IsNullOrEmpty((string)responseData.germanTitle))
|
|
||||||
{
|
|
||||||
germanTitle = responseData.germanTitle;
|
|
||||||
hasGermanTitle = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
germanTitle = expectedTitle;
|
|
||||||
hasGermanTitle = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasGermanUmlaut = germanTitle?.HasGermanUmlauts() ?? false;
|
|
||||||
|
|
||||||
var result = (hasGermanUmlaut, germanTitle, expectedTitle);
|
|
||||||
_cache.Set(cacheKey, result, new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
Size = 1,
|
|
||||||
SlidingExpiration = hasGermanTitle ? TimeSpan.FromDays(30) : TimeSpan.FromDays(7)
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(bool hasGermanUmlaut, string? GermanTitle, string ExpectedTitle)> QueryGermanShowTitleByTitle(string title)
|
|
||||||
{
|
|
||||||
// TVDB doesn't use ß
|
|
||||||
var tvdbCleanTitle = title.Replace("ß", "ss");
|
|
||||||
|
|
||||||
var cacheKey = $"show_{tvdbCleanTitle}";
|
|
||||||
if (_cache.TryGetValue(cacheKey, out (bool hasGermanUmlaut, string? GermanTitle, string ExpectedTitle) cachedResult))
|
|
||||||
{
|
|
||||||
return cachedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
var apiUrl = $"{_umlautAdaptarrApiHost}/tvshow_german.php?title={tvdbCleanTitle}";
|
|
||||||
var apiResponse = await _httpClient.GetStringAsync(apiUrl);
|
|
||||||
var responseData = JsonConvert.DeserializeObject<dynamic>(apiResponse);
|
|
||||||
|
|
||||||
if (responseData == null)
|
|
||||||
{
|
|
||||||
_logger.LogError($"Parsing UmlautAdaptarr TitleQuery API response for title {title} resulted in null");
|
|
||||||
return (false, null, string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (responseData.status == "success" && !string.IsNullOrEmpty((string)responseData.germanTitle))
|
|
||||||
{
|
|
||||||
var tvdbId = (string)responseData.tvdbId;
|
|
||||||
if (tvdbId == null)
|
|
||||||
{
|
|
||||||
_logger.LogError($"Parsing UmlautAdaptarr TitleQuery API response tvdbId {responseData} resulted in null");
|
|
||||||
return (false, null, string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
var sonarrUrl = $"{_sonarrHost}/api/v3/series?tvdbId={tvdbId}&includeSeasonImages=false&apikey={_sonarrApiKey}";
|
|
||||||
var response = await _httpClient.GetStringAsync(sonarrUrl);
|
|
||||||
var shows = JsonConvert.DeserializeObject<dynamic>(response);
|
|
||||||
|
|
||||||
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;
|
|
||||||
var originalLanguage = (string)shows[0].originalLanguage.name;
|
|
||||||
|
|
||||||
if (originalLanguage != "German")
|
|
||||||
{
|
|
||||||
germanTitle = responseData.germanTitle;
|
|
||||||
hasGermanTitle = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
germanTitle = expectedTitle;
|
|
||||||
hasGermanTitle = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasGermanUmlaut = germanTitle?.HasGermanUmlauts() ?? false;
|
|
||||||
|
|
||||||
var result = (hasGermanUmlaut, germanTitle, expectedTitle);
|
|
||||||
_cache.Set(cacheKey, result, new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
Size = 1,
|
|
||||||
SlidingExpiration = hasGermanTitle ? TimeSpan.FromDays(30) : TimeSpan.FromDays(7)
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning($"UmlautAdaptarr TitleQuery { apiUrl } didn't succeed.");
|
|
||||||
return (false, null, string.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
162
UmlautAdaptarr/Services/TitleQueryServiceLegacy.cs
Normal file
162
UmlautAdaptarr/Services/TitleQueryServiceLegacy.cs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
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?.HasGermanUmlauts() ?? 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -40,5 +40,14 @@ namespace UmlautAdaptarr.Utilities
|
|||||||
|
|
||||||
return BuildUrl(domain, queryParameters);
|
return BuildUrl(domain, queryParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string RedactApiKey(string targetUri)
|
||||||
|
{
|
||||||
|
var apiKeyPattern = @"(apikey=)[^&]*";
|
||||||
|
|
||||||
|
var redactedUri = Regex.Replace(targetUri, apiKeyPattern, "$1[REDACTED]");
|
||||||
|
|
||||||
|
return redactedUri;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"Kestrel": {
|
"Kestrel": {
|
||||||
"Endpoints": {
|
"Endpoints": {
|
||||||
"Http": {
|
"Http": {
|
||||||
"Url": "http://localhost:5005"
|
"Url": "http://*:5005"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
9
docker-compose.yml
Normal file
9
docker-compose.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
umlautadaptarr:
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
SONARR_HOST: "http://localhost:8989"
|
||||||
|
SONARR_API_KEY: ""
|
||||||
|
ports:
|
||||||
|
- "5005:5005"
|
||||||
Reference in New Issue
Block a user