Intermediate commit :-)
This commit is contained in:
@@ -1,36 +1,135 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using UmlautAdaptarr.Models;
|
||||||
using UmlautAdaptarr.Services;
|
using UmlautAdaptarr.Services;
|
||||||
using UmlautAdaptarr.Utilities;
|
using UmlautAdaptarr.Utilities;
|
||||||
|
|
||||||
namespace UmlautAdaptarr.Controllers
|
namespace UmlautAdaptarr.Controllers
|
||||||
{
|
{
|
||||||
public abstract class SearchControllerBase(ProxyService proxyService) : ControllerBase
|
public abstract class SearchControllerBase(ProxyService proxyService, TitleMatchingService titleMatchingService) : ControllerBase
|
||||||
{
|
{
|
||||||
protected readonly ProxyService _proxyService = proxyService;
|
protected readonly ProxyService _proxyService = proxyService;
|
||||||
|
protected readonly TitleMatchingService _titleMatchingService = titleMatchingService;
|
||||||
|
|
||||||
protected async Task<IActionResult> BaseSearch(string options, string domain, IDictionary<string, string> queryParameters)
|
protected async Task<IActionResult> BaseSearch(string options,
|
||||||
|
string domain,
|
||||||
|
IDictionary<string, string> queryParameters,
|
||||||
|
string? germanTitle = null,
|
||||||
|
string? expectedTitle = null,
|
||||||
|
bool hasGermanUmlaut = false)
|
||||||
{
|
{
|
||||||
if (!UrlUtilities.IsValidDomain(domain))
|
try
|
||||||
{
|
{
|
||||||
return NotFound($"{domain} is not a valid URL.");
|
if (!UrlUtilities.IsValidDomain(domain))
|
||||||
|
{
|
||||||
|
return NotFound($"{domain} is not a valid URL.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate title variations for renaming process
|
||||||
|
var germanTitleVariations = !string.IsNullOrEmpty(germanTitle) ? _titleMatchingService.GenerateTitleVariations(germanTitle) : new List<string>();
|
||||||
|
|
||||||
|
// Check if "q" parameter exists for multiple search request handling
|
||||||
|
if (hasGermanUmlaut && !string.IsNullOrEmpty(germanTitle) && !string.IsNullOrEmpty(expectedTitle) && queryParameters.ContainsKey("q"))
|
||||||
|
{
|
||||||
|
// Add original search query to title variations
|
||||||
|
var q = queryParameters["q"];
|
||||||
|
if (!germanTitleVariations.Contains(q))
|
||||||
|
{
|
||||||
|
germanTitleVariations.Add(queryParameters["q"]!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiple search requests based on German title variations
|
||||||
|
var aggregatedResult = await AggregateSearchResults(domain, queryParameters, germanTitleVariations, expectedTitle);
|
||||||
|
// Rename titles in the aggregated content
|
||||||
|
var processedContent = ProcessContent(aggregatedResult.Content, germanTitleVariations, expectedTitle);
|
||||||
|
return Content(processedContent, 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// TODO error logging
|
||||||
|
Console.WriteLine(ex.ToString());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 ?
|
||||||
Encoding.GetEncoding(responseMessage.Content.Headers.ContentType.CharSet) :
|
Encoding.GetEncoding(responseMessage.Content.Headers.ContentType.CharSet) :
|
||||||
Encoding.UTF8;
|
Encoding.UTF8;
|
||||||
var contentType = responseMessage.Content.Headers.ContentType?.MediaType ?? "application/xml";
|
string contentType = responseMessage.Content.Headers.ContentType?.MediaType ?? "application/xml";
|
||||||
|
|
||||||
return Content(content, contentType, encoding);
|
return Content(content, contentType, encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private string ProcessContent(string content, List<string> germanTitleVariations, string? expectedTitle)
|
||||||
|
{
|
||||||
|
// Check if German title and expected title are provided for renaming
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
string defaultContentType = "application/xml";
|
||||||
|
Encoding defaultEncoding = Encoding.UTF8;
|
||||||
|
bool encodingSet = false;
|
||||||
|
|
||||||
|
var aggregatedResult = new AggregatedSearchResult(defaultContentType, defaultEncoding);
|
||||||
|
|
||||||
|
foreach (var titleVariation in germanTitleVariations.Distinct())
|
||||||
|
{
|
||||||
|
queryParameters["q"] = titleVariation; // Replace the "q" parameter for each variation
|
||||||
|
var requestUrl = UrlUtilities.BuildUrl(domain, queryParameters);
|
||||||
|
var responseMessage = await _proxyService.ProxyRequestAsync(HttpContext, requestUrl);
|
||||||
|
var content = await responseMessage.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Only update encoding from the first response
|
||||||
|
if (!encodingSet && responseMessage.Content.Headers.ContentType?.CharSet != null)
|
||||||
|
{
|
||||||
|
aggregatedResult.ContentEncoding = Encoding.GetEncoding(responseMessage.Content.Headers.ContentType.CharSet); ;
|
||||||
|
aggregatedResult.ContentType = responseMessage.Content.Headers.ContentType?.MediaType ?? defaultContentType;
|
||||||
|
encodingSet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process and rename titles in the content
|
||||||
|
content = _titleMatchingService.RenameTitlesInContent(content, germanTitleVariations, expectedTitle);
|
||||||
|
|
||||||
|
// Aggregate the items into a single document
|
||||||
|
aggregatedResult.AggregateItems(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregatedResult;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SearchController(ProxyService proxyService, TitleQueryService titleQueryService) : SearchControllerBase(proxyService)
|
public class SearchController(ProxyService proxyService,
|
||||||
|
TitleMatchingService titleMatchingService,
|
||||||
|
TitleQueryService titleQueryService) : 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)
|
||||||
@@ -66,16 +165,47 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
q => q.Key,
|
q => q.Key,
|
||||||
q => string.Join(",", q.Value));
|
q => string.Join(",", q.Value));
|
||||||
|
|
||||||
|
string? searchKey = null;
|
||||||
|
string? searchValue = null;
|
||||||
|
|
||||||
if (queryParameters.TryGetValue("tvdbid", out string tvdbId))
|
if (queryParameters.TryGetValue("tvdbid", out string? tvdbId))
|
||||||
{
|
{
|
||||||
var (HasGermanUmlaut, GermanTitle, ExpectedTitle) = await titleQueryService.QueryShow(tvdbId);
|
searchKey = "tvdbid";
|
||||||
|
searchValue = tvdbId;
|
||||||
|
}
|
||||||
|
else if (queryParameters.TryGetValue("q", out string? title))
|
||||||
|
{
|
||||||
|
searchKey = "q";
|
||||||
|
searchValue = title;
|
||||||
|
}
|
||||||
|
|
||||||
if (GermanTitle == null && ExpectedTitle == null)
|
// Perform the search if a valid search key was identified
|
||||||
|
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))
|
||||||
{
|
{
|
||||||
return NotFound($"Show with TVDB ID {tvdbId} not found.");
|
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);
|
||||||
|
aggregatedResult.AggregateItems((initialSearchResult as ContentResult)?.Content ?? "");
|
||||||
|
return Content(aggregatedResult.Content, aggregatedResult.ContentType, aggregatedResult.ContentEncoding);
|
||||||
|
}
|
||||||
|
return initialSearchResult;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await BaseSearch(options, domain, queryParameters);
|
return await BaseSearch(options, domain, queryParameters);
|
||||||
|
|||||||
45
UmlautAdaptarr/Models/AggregatedSearchResult.cs
Normal file
45
UmlautAdaptarr/Models/AggregatedSearchResult.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
namespace UmlautAdaptarr.Models
|
||||||
|
{
|
||||||
|
public class AggregatedSearchResult
|
||||||
|
{
|
||||||
|
public XDocument ContentDocument { get; private set; }
|
||||||
|
public string ContentType { get; set; }
|
||||||
|
public Encoding ContentEncoding { get; set; }
|
||||||
|
private HashSet<string> _uniqueItems;
|
||||||
|
|
||||||
|
public AggregatedSearchResult(string contentType, Encoding contentEncoding)
|
||||||
|
{
|
||||||
|
ContentType = contentType;
|
||||||
|
ContentEncoding = contentEncoding;
|
||||||
|
_uniqueItems = new HashSet<string>();
|
||||||
|
|
||||||
|
// Initialize ContentDocument with a basic RSS structure
|
||||||
|
ContentDocument = new XDocument(new XElement("rss", new XElement("channel")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Content => ContentDocument.ToString();
|
||||||
|
|
||||||
|
public void AggregateItems(string content)
|
||||||
|
{
|
||||||
|
var xDoc = XDocument.Parse(content);
|
||||||
|
var items = xDoc.Descendants("item");
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
var itemAsString = item.ToString();
|
||||||
|
if (_uniqueItems.Add(itemAsString))
|
||||||
|
{
|
||||||
|
ContentDocument.Root.Element("channel").Add(item);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,16 @@ internal class Program
|
|||||||
{
|
{
|
||||||
private static void Main(string[] args)
|
private static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
// TODO:
|
||||||
|
// add option to sort by nzb age
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// add delay between requests
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
var configuration = builder.Configuration;
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddHttpClient("HttpClient").ConfigurePrimaryHttpMessageHandler(() =>
|
builder.Services.AddHttpClient("HttpClient").ConfigurePrimaryHttpMessageHandler(() =>
|
||||||
{
|
{
|
||||||
@@ -28,6 +36,7 @@ internal class Program
|
|||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddScoped<TitleQueryService>();
|
builder.Services.AddScoped<TitleQueryService>();
|
||||||
builder.Services.AddScoped<ProxyService>();
|
builder.Services.AddScoped<ProxyService>();
|
||||||
|
builder.Services.AddScoped<TitleMatchingService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
"http": {
|
"http": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"launchUrl": "optionsTODO/example.com/api?t=movie&apikey=132&imdbid=123&limit=100",
|
"_launchUrl": "optionsTODO/example.com/api?t=movie&apikey=132&imdbid=123&limit=100",
|
||||||
|
"launchUrl": "/",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient = clientFactory.CreateClient("HttpClient") ?? throw new ArgumentNullException();
|
private readonly HttpClient _httpClient = clientFactory.CreateClient("HttpClient") ?? throw new ArgumentNullException();
|
||||||
private readonly string _userAgent = configuration["Settings:UserAgent"] ?? throw new ArgumentException("UserAgent must be set in appsettings.json");
|
private readonly string _userAgent = configuration["Settings:UserAgent"] ?? throw new ArgumentException("UserAgent must be set in appsettings.json");
|
||||||
|
// TODO: Add cache!
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> ProxyRequestAsync(HttpContext context, string targetUri)
|
public async Task<HttpResponseMessage> ProxyRequestAsync(HttpContext context, string targetUri)
|
||||||
{
|
{
|
||||||
@@ -35,6 +36,8 @@
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var responseMessage = _httpClient.Send(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
|
var responseMessage = _httpClient.Send(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
|
||||||
|
|
||||||
|
// TODO: Handle 503 etc
|
||||||
responseMessage.EnsureSuccessStatusCode();
|
responseMessage.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
// Modify the response content if necessary
|
// Modify the response content if necessary
|
||||||
|
|||||||
125
UmlautAdaptarr/Services/TitleMatchingService.cs
Normal file
125
UmlautAdaptarr/Services/TitleMatchingService.cs
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using UmlautAdaptarr.Utilities;
|
||||||
|
|
||||||
|
namespace UmlautAdaptarr.Services
|
||||||
|
{
|
||||||
|
public partial class TitleMatchingService
|
||||||
|
{
|
||||||
|
public List<string> GenerateTitleVariations(string germanTitle)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
foreach (var item in xDoc.Descendants("item"))
|
||||||
|
{
|
||||||
|
var titleElement = item.Element("title");
|
||||||
|
if (titleElement != null)
|
||||||
|
{
|
||||||
|
var originalTitle = titleElement.Value;
|
||||||
|
var normalizedOriginalTitle = NormalizeTitle(originalTitle);
|
||||||
|
|
||||||
|
// Attempt to find a variation that matches the start of the original title
|
||||||
|
foreach (var variation in germanTitleVariations)
|
||||||
|
{
|
||||||
|
// Variation is already normalized at creation
|
||||||
|
var pattern = "^" + Regex.Escape(variation).Replace("\\ ", "[._ ]");
|
||||||
|
|
||||||
|
// Check if the originalTitle starts with the variation (ignoring case and separators)
|
||||||
|
if (Regex.IsMatch(normalizedOriginalTitle, pattern, RegexOptions.IgnoreCase))
|
||||||
|
{
|
||||||
|
// Find the first separator used in the original title for consistent replacement
|
||||||
|
var separator = FindFirstSeparator(originalTitle);
|
||||||
|
// Reconstruct the expected title using the original separator
|
||||||
|
var newTitlePrefix = expectedTitle.Replace(" ", separator.ToString());
|
||||||
|
|
||||||
|
// Extract the suffix from the original title starting right after the matched variation length
|
||||||
|
var variationLength = variation.Length;
|
||||||
|
var suffix = originalTitle[Math.Min(variationLength, originalTitle.Length)..];
|
||||||
|
|
||||||
|
// Clean up any leading separators from the suffix
|
||||||
|
suffix = Regex.Replace(suffix, "^[._ ]+", "");
|
||||||
|
|
||||||
|
// TODO EVALUTE! definitely make this optional - this adds GERMAN to the title is the title is german to make sure it's recognized as german
|
||||||
|
// can lead to problems with shows such as "dark" that have international dubs
|
||||||
|
/*
|
||||||
|
// Check if "german" is not in the original title, ignoring case
|
||||||
|
if (!Regex.IsMatch(originalTitle, "german", RegexOptions.IgnoreCase))
|
||||||
|
{
|
||||||
|
// Insert "GERMAN" after the newTitlePrefix
|
||||||
|
newTitlePrefix += separator + "GERMAN";
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Construct the new title with the original suffix
|
||||||
|
var newTitle = newTitlePrefix + (string.IsNullOrEmpty(suffix) ? "" : separator + suffix);
|
||||||
|
|
||||||
|
// Update the title element's value with the new title
|
||||||
|
titleElement.Value = newTitle + $"({originalTitle.Substring(0, variationLength)})";
|
||||||
|
break; // Break after the first successful match and modification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return xDoc.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static string NormalizeTitle(string title)
|
||||||
|
{
|
||||||
|
title = title.RemoveAccentButKeepGermanUmlauts();
|
||||||
|
// Replace all known separators with a consistent one for normalization
|
||||||
|
return WordSeperationCharRegex().Replace(title, " ".ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static char FindFirstSeparator(string title)
|
||||||
|
{
|
||||||
|
var match = WordSeperationCharRegex().Match(title);
|
||||||
|
return match.Success ? match.Value.First() : ' '; // Default to space if no separator found
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReconstructTitleWithSeparator(string title, char separator)
|
||||||
|
{
|
||||||
|
// Replace spaces with the original separator found in the title
|
||||||
|
return title.Replace(' ', separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[GeneratedRegex("[._ ]")]
|
||||||
|
private static partial Regex WordSeperationCharRegex();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,18 +15,21 @@ namespace UmlautAdaptarr.Services
|
|||||||
private readonly ILogger<TitleQueryService> _logger;
|
private readonly ILogger<TitleQueryService> _logger;
|
||||||
private readonly string _sonarrHost;
|
private readonly string _sonarrHost;
|
||||||
private readonly string _sonarrApiKey;
|
private readonly string _sonarrApiKey;
|
||||||
|
private readonly string _umlautAdaptarrApiHost;
|
||||||
|
|
||||||
public TitleQueryService(HttpClient httpClient, IMemoryCache memoryCache, ILogger<TitleQueryService> logger)
|
public TitleQueryService(IMemoryCache memoryCache, ILogger<TitleQueryService> logger, IConfiguration configuration, IHttpClientFactory clientFactory)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = clientFactory.CreateClient("HttpClient") ?? throw new ArgumentNullException();
|
||||||
_cache = memoryCache;
|
_cache = memoryCache;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
_sonarrHost = Environment.GetEnvironmentVariable("SONARR_HOST");
|
_sonarrHost = configuration.GetValue<string>("SONARR_HOST");
|
||||||
_sonarrApiKey = Environment.GetEnvironmentVariable("SONARR_API_KEY");
|
_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)> QueryShow(string tvdbId)
|
public async Task<(bool hasGermanUmlaut, string? GermanTitle, string ExpectedTitle)> QueryGermanShowTitleByTVDBId(string tvdbId)
|
||||||
{
|
{
|
||||||
var cacheKey = $"show_{tvdbId}";
|
var cacheKey = $"show_{tvdbId}";
|
||||||
if (_cache.TryGetValue(cacheKey, out (bool hasGermanUmlaut, string? GermanTitle, string ExpectedTitle) cachedResult))
|
if (_cache.TryGetValue(cacheKey, out (bool hasGermanUmlaut, string? GermanTitle, string ExpectedTitle) cachedResult))
|
||||||
@@ -48,7 +51,7 @@ namespace UmlautAdaptarr.Services
|
|||||||
return (false, null, string.Empty);
|
return (false, null, string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
var expectedTitle = shows[0].title as string;
|
var expectedTitle = (string)shows[0].title;
|
||||||
if (expectedTitle == null)
|
if (expectedTitle == null)
|
||||||
{
|
{
|
||||||
_logger.LogError($"Sonarr Title for TVDB ID {tvdbId} is null");
|
_logger.LogError($"Sonarr Title for TVDB ID {tvdbId} is null");
|
||||||
@@ -57,22 +60,23 @@ namespace UmlautAdaptarr.Services
|
|||||||
|
|
||||||
string? germanTitle = null;
|
string? germanTitle = null;
|
||||||
var hasGermanTitle = false;
|
var hasGermanTitle = false;
|
||||||
|
var originalLanguage = (string)shows[0].originalLanguage.name;
|
||||||
|
|
||||||
if ((string)shows[0].originalLanguage.name != "German")
|
if (originalLanguage != "German")
|
||||||
{
|
{
|
||||||
var thetvdbUrl = $"https://umlautadaptarr.pcjones.de/get_german_title.php?tvdbid={tvdbId}";
|
var apiUrl = $"{_umlautAdaptarrApiHost}/tvshow_german.php?tvdbid={tvdbId}";
|
||||||
var tvdbResponse = await _httpClient.GetStringAsync(thetvdbUrl);
|
var apiResponse = await _httpClient.GetStringAsync(apiUrl);
|
||||||
var tvdbData = JsonConvert.DeserializeObject<dynamic>(tvdbResponse);
|
var responseData = JsonConvert.DeserializeObject<dynamic>(apiResponse);
|
||||||
|
|
||||||
if (tvdbData == null)
|
if (responseData == null)
|
||||||
{
|
{
|
||||||
_logger.LogError($"Parsing UmlautAdaptarr TitleQuery API response for TVDB ID {tvdbId} resulted in null");
|
_logger.LogError($"Parsing UmlautAdaptarr TitleQuery API response for TVDB ID {tvdbId} resulted in null");
|
||||||
return (false, null, string.Empty);
|
return (false, null, string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tvdbData.status == "success")
|
if (responseData.status == "success" && !string.IsNullOrEmpty((string)responseData.germanTitle))
|
||||||
{
|
{
|
||||||
germanTitle = tvdbData.germanTitle;
|
germanTitle = responseData.germanTitle;
|
||||||
hasGermanTitle = true;
|
hasGermanTitle = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,11 +91,97 @@ namespace UmlautAdaptarr.Services
|
|||||||
var result = (hasGermanUmlaut, germanTitle, expectedTitle);
|
var result = (hasGermanUmlaut, germanTitle, expectedTitle);
|
||||||
_cache.Set(cacheKey, result, new MemoryCacheEntryOptions
|
_cache.Set(cacheKey, result, new MemoryCacheEntryOptions
|
||||||
{
|
{
|
||||||
|
Size = 1,
|
||||||
SlidingExpiration = hasGermanTitle ? TimeSpan.FromDays(30) : TimeSpan.FromDays(7)
|
SlidingExpiration = hasGermanTitle ? TimeSpan.FromDays(30) : TimeSpan.FromDays(7)
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
<UserSecretsId>c5f05dc6-731e-425e-8b8c-a4c09f440adc</UserSecretsId>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -12,8 +13,4 @@
|
|||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Models\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -9,13 +9,28 @@ namespace UmlautAdaptarr.Utilities
|
|||||||
{
|
{
|
||||||
return context.Request.Query[key].FirstOrDefault() ?? string.Empty;
|
return context.Request.Query[key].FirstOrDefault() ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
public static string RemoveAccent(this string text)
|
||||||
|
{
|
||||||
|
var normalizedString = text.Normalize(NormalizationForm.FormD);
|
||||||
|
var stringBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (var c in normalizedString)
|
||||||
|
{
|
||||||
|
var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
|
||||||
|
|
||||||
|
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
|
||||||
|
{
|
||||||
|
stringBuilder.Append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static string RemoveAccentButKeepGermanUmlauts(this string text)
|
public static string RemoveAccentButKeepGermanUmlauts(this string text)
|
||||||
{
|
{
|
||||||
// TODO: evaluate if this is needed (here)
|
var normalizedString = text.Normalize(NormalizationForm.FormD);
|
||||||
var stringWithoutSz = text.Replace("ß", "ss");
|
|
||||||
|
|
||||||
var normalizedString = stringWithoutSz.Normalize(NormalizationForm.FormD);
|
|
||||||
var stringBuilder = new StringBuilder();
|
var stringBuilder = new StringBuilder();
|
||||||
|
|
||||||
foreach (var c in normalizedString)
|
foreach (var c in normalizedString)
|
||||||
@@ -43,6 +58,18 @@ namespace UmlautAdaptarr.Utilities
|
|||||||
.Replace("ß", "ss");
|
.Replace("ß", "ss");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string RemoveGermanUmlautDots(this string text)
|
||||||
|
{
|
||||||
|
return text
|
||||||
|
.Replace("ö", "o")
|
||||||
|
.Replace("ü", "u")
|
||||||
|
.Replace("ä", "a")
|
||||||
|
.Replace("Ö", "O")
|
||||||
|
.Replace("Ü", "U")
|
||||||
|
.Replace("Ä", "A")
|
||||||
|
.Replace("ß", "ss");
|
||||||
|
}
|
||||||
|
|
||||||
public static bool HasGermanUmlauts(this string text)
|
public static bool HasGermanUmlauts(this string text)
|
||||||
{
|
{
|
||||||
if (text == null) return false;
|
if (text == null) return false;
|
||||||
|
|||||||
@@ -15,6 +15,6 @@
|
|||||||
},
|
},
|
||||||
"Settings": {
|
"Settings": {
|
||||||
"UserAgent": "UmlautAdaptarr/1.0",
|
"UserAgent": "UmlautAdaptarr/1.0",
|
||||||
"TitleQueryHost": "https://umlautadaptarr.pcjones.de"
|
"UmlautAdaptarrApiHost": "https://umlautadaptarr.pcjones.de/api/v1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
test.txt
Normal file
22
test.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:newznab="http://www.newznab.com/DTD/2010/feeds/attributes/">
|
||||||
|
<channel>
|
||||||
|
<atom:link href="https://scenenzbs.com/api/?t=tvsearch&cat=5030%2c5040%2c5100&extended=1&apikey=c1e3d46f8df56a904cc35a6e3ccbc401&offset=0&limit=100&tvdbid=323168&tvmazeid=7194&season=1&ep=130" rel="self" type="application/rss+xml" />
|
||||||
|
<title>SceneNZBs</title>
|
||||||
|
<description>SceneNZBs Feed</description>
|
||||||
|
<link>https://scenenzbs.com/</link>
|
||||||
|
<language>en-gb</language>
|
||||||
|
<webMaster>support@scenenzbs.com (SceneNZBs)</webMaster>
|
||||||
|
<category></category>
|
||||||
|
<image>
|
||||||
|
<url>https://scenenzbs.com/templates/default/images/banner.jpg</url>
|
||||||
|
<title>SceneNZBs</title>
|
||||||
|
<link>https://scenenzbs.com/</link>
|
||||||
|
<description>Visit SceneNZBs - </description>
|
||||||
|
</image>
|
||||||
|
<newznab:apilimits apicurrent="342" apimax="10000" grabcurrent="3" grabmax="2000" apioldesttime="Tue, 06 Feb 2024 01:33:47 +0100" graboldesttime="Tue, 06 Feb 2024 17:54:25 +0100"/>
|
||||||
|
|
||||||
|
<newznab:response offset="0" total="0" />
|
||||||
|
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
Reference in New Issue
Block a user