Intermediate commit
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using UmlautAdaptarr.Models;
|
using UmlautAdaptarr.Models;
|
||||||
@@ -34,7 +36,7 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
// Rename titles in the single search content
|
// Rename titles in the single search content
|
||||||
if (!string.IsNullOrEmpty(initialSearchResult?.Content))
|
if (!string.IsNullOrEmpty(initialSearchResult?.Content))
|
||||||
{
|
{
|
||||||
inititalProcessedContent = ProcessContent(initialSearchResult.Content, searchItem?.TitleMatchVariations, searchItem?.ExpectedTitle);
|
inititalProcessedContent = ProcessContent(initialSearchResult.Content, searchItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
var additionalTextSearch = searchItem != null
|
var additionalTextSearch = searchItem != null
|
||||||
@@ -76,7 +78,7 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle multiple search requests based on German title variations
|
// Handle multiple search requests based on German title variations
|
||||||
var aggregatedResult = await AggregateSearchResults(domain, queryParameters, titleSearchVariations, searchItem.TitleMatchVariations, expectedTitle);
|
var aggregatedResult = await AggregateSearchResults(domain, queryParameters, titleSearchVariations, searchItem);
|
||||||
aggregatedResult.AggregateItems(inititalProcessedContent);
|
aggregatedResult.AggregateItems(inititalProcessedContent);
|
||||||
|
|
||||||
return Content(aggregatedResult.Content, aggregatedResult.ContentType, aggregatedResult.ContentEncoding);
|
return Content(aggregatedResult.Content, aggregatedResult.ContentType, aggregatedResult.ContentEncoding);
|
||||||
@@ -109,17 +111,17 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private string ProcessContent(string content, string[]? titleMatchVariations = null, string? expectedTitle = null)
|
private string ProcessContent(string content, SearchItem? searchItem)
|
||||||
{
|
{
|
||||||
return titleMatchingService.RenameTitlesInContent(content, titleMatchVariations, expectedTitle);
|
return titleMatchingService.RenameTitlesInContent(content, searchItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AggregatedSearchResult> AggregateSearchResults(
|
public async Task<AggregatedSearchResult> AggregateSearchResults(
|
||||||
string domain,
|
string domain,
|
||||||
IDictionary<string, string> queryParameters,
|
IDictionary<string, string> queryParameters,
|
||||||
IEnumerable<string> titleSearchVariations,
|
IEnumerable<string> titleSearchVariations,
|
||||||
string[] titleMatchVariations,
|
SearchItem? searchItem
|
||||||
string expectedTitle)
|
)
|
||||||
{
|
{
|
||||||
string defaultContentType = "application/xml";
|
string defaultContentType = "application/xml";
|
||||||
Encoding defaultEncoding = Encoding.UTF8;
|
Encoding defaultEncoding = Encoding.UTF8;
|
||||||
@@ -143,7 +145,7 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process and rename titles in the content
|
// Process and rename titles in the content
|
||||||
content = ProcessContent(content, titleMatchVariations, expectedTitle);
|
content = ProcessContent(content, searchItem);
|
||||||
|
|
||||||
// Aggregate the items into a single document
|
// Aggregate the items into a single document
|
||||||
aggregatedResult.AggregateItems(content);
|
aggregatedResult.AggregateItems(content);
|
||||||
@@ -157,6 +159,8 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
TitleMatchingService titleMatchingService,
|
TitleMatchingService titleMatchingService,
|
||||||
SearchItemLookupService searchItemLookupService) : SearchControllerBase(proxyService, titleMatchingService)
|
SearchItemLookupService searchItemLookupService) : SearchControllerBase(proxyService, titleMatchingService)
|
||||||
{
|
{
|
||||||
|
public readonly string[] AUDIO_CATEGORY_IDS = ["3000", "3010", "3020", "3040", "3050"];
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> MovieSearch([FromRoute] string options, [FromRoute] string domain)
|
public async Task<IActionResult> MovieSearch([FromRoute] string options, [FromRoute] string domain)
|
||||||
{
|
{
|
||||||
@@ -169,10 +173,27 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GenericSearch([FromRoute] string options, [FromRoute] string domain)
|
public async Task<IActionResult> GenericSearch([FromRoute] string options, [FromRoute] string domain)
|
||||||
{
|
{
|
||||||
|
|
||||||
var queryParameters = HttpContext.Request.Query.ToDictionary(
|
var queryParameters = HttpContext.Request.Query.ToDictionary(
|
||||||
q => q.Key,
|
q => q.Key,
|
||||||
q => string.Join(",", q.Value));
|
q => string.Join(",", q.Value));
|
||||||
return await BaseSearch(options, domain, queryParameters);
|
|
||||||
|
SearchItem? searchItem = null;
|
||||||
|
|
||||||
|
if (queryParameters.TryGetValue("q", out string? title) && !string.IsNullOrEmpty(title))
|
||||||
|
{
|
||||||
|
if (queryParameters.TryGetValue("cat", out string? categories) && !string.IsNullOrEmpty(categories))
|
||||||
|
{
|
||||||
|
// Search for audio
|
||||||
|
if (categories.Split(',').Any(category => AUDIO_CATEGORY_IDS.Contains(category)))
|
||||||
|
{
|
||||||
|
var mediaType = "audio";
|
||||||
|
searchItem = await searchItemLookupService.GetOrFetchSearchItemByExternalId(mediaType, title.ToLower());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await BaseSearch(options, domain, queryParameters, searchItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@@ -192,7 +213,7 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
q => string.Join(",", q.Value));
|
q => string.Join(",", q.Value));
|
||||||
|
|
||||||
SearchItem? searchItem = null;
|
SearchItem? searchItem = null;
|
||||||
string mediaType = "tv";
|
var mediaType = "tv";
|
||||||
|
|
||||||
if (queryParameters.TryGetValue("tvdbid", out string? tvdbId) && !string.IsNullOrEmpty(tvdbId))
|
if (queryParameters.TryGetValue("tvdbid", out string? tvdbId) && !string.IsNullOrEmpty(tvdbId))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.RegularExpressions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using UmlautAdaptarr.Utilities;
|
using UmlautAdaptarr.Utilities;
|
||||||
|
|
||||||
namespace UmlautAdaptarr.Models
|
namespace UmlautAdaptarr.Models
|
||||||
@@ -8,24 +9,51 @@ namespace UmlautAdaptarr.Models
|
|||||||
public int ArrId { get; set; }
|
public int ArrId { get; set; }
|
||||||
public string ExternalId { get; set; }
|
public string ExternalId { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public bool HasGermanUmlaut => Title?.HasGermanUmlauts() ?? false;
|
public bool HasUmlaut => Title?.HasUmlauts() ?? false;
|
||||||
public string ExpectedTitle { get; set; }
|
public string ExpectedTitle { get; set; }
|
||||||
|
public string? ExpectedAuthor { get; set; }
|
||||||
public string? GermanTitle { get; set; }
|
public string? GermanTitle { get; set; }
|
||||||
public string[] TitleSearchVariations { get; set; }
|
public string[] TitleSearchVariations { get; set; }
|
||||||
public string[] TitleMatchVariations { get; set; }
|
public string[] TitleMatchVariations { get; set; }
|
||||||
|
public string[] AuthorMatchVariations { get; set; }
|
||||||
public string MediaType { get; set; }
|
public string MediaType { get; set; }
|
||||||
// TODO public MediaType instead of string
|
// TODO public MediaType instead of string
|
||||||
|
|
||||||
public SearchItem(int arrId, string externalId, string title, string expectedTitle, string? germanTitle, string mediaType, string[]? aliases)
|
public SearchItem(
|
||||||
|
int arrId,
|
||||||
|
string externalId,
|
||||||
|
string title,
|
||||||
|
string expectedTitle,
|
||||||
|
string? germanTitle,
|
||||||
|
string mediaType,
|
||||||
|
string[]? aliases,
|
||||||
|
string? expectedAuthor = null)
|
||||||
{
|
{
|
||||||
ArrId = arrId;
|
ArrId = arrId;
|
||||||
ExternalId = externalId;
|
ExternalId = externalId;
|
||||||
Title = title;
|
Title = title;
|
||||||
ExpectedTitle = expectedTitle;
|
ExpectedTitle = expectedTitle;
|
||||||
|
ExpectedAuthor = expectedAuthor;
|
||||||
GermanTitle = germanTitle;
|
GermanTitle = germanTitle;
|
||||||
TitleSearchVariations = GenerateTitleVariations(germanTitle).ToArray();
|
|
||||||
MediaType = mediaType;
|
MediaType = mediaType;
|
||||||
|
if (mediaType == "audio" && expectedAuthor != null)
|
||||||
|
{
|
||||||
|
// e.g. Die Ärzte - best of die Ärzte
|
||||||
|
if (expectedTitle.Contains(expectedAuthor))
|
||||||
|
{
|
||||||
|
var titleWithoutAuthorName = expectedTitle.Replace(expectedAuthor, string.Empty).RemoveExtraWhitespaces().Trim();
|
||||||
|
TitleMatchVariations = GenerateVariations(titleWithoutAuthorName, mediaType).ToArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TitleMatchVariations = GenerateVariations(expectedTitle, mediaType).ToArray();
|
||||||
|
}
|
||||||
|
TitleSearchVariations = GenerateVariations($"{expectedAuthor} {expectedTitle}", mediaType).ToArray();
|
||||||
|
AuthorMatchVariations = GenerateVariations(expectedAuthor, mediaType).ToArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TitleSearchVariations = GenerateVariations(germanTitle, mediaType).ToArray();
|
||||||
var allTitleVariations = new List<string>(TitleSearchVariations);
|
var allTitleVariations = new List<string>(TitleSearchVariations);
|
||||||
|
|
||||||
// If aliases are not null, generate variations for each and add them to the list
|
// If aliases are not null, generate variations for each and add them to the list
|
||||||
@@ -34,14 +62,15 @@ namespace UmlautAdaptarr.Models
|
|||||||
{
|
{
|
||||||
foreach (var alias in aliases)
|
foreach (var alias in aliases)
|
||||||
{
|
{
|
||||||
allTitleVariations.AddRange(GenerateTitleVariations(alias));
|
allTitleVariations.AddRange(GenerateVariations(alias, mediaType));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TitleMatchVariations = allTitleVariations.Distinct().ToArray();
|
TitleMatchVariations = allTitleVariations.Distinct().ToArray();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private IEnumerable<string> GenerateTitleVariations(string? germanTitle)
|
private IEnumerable<string> GenerateVariations(string? germanTitle, string mediaType)
|
||||||
{
|
{
|
||||||
if (germanTitle == null)
|
if (germanTitle == null)
|
||||||
{
|
{
|
||||||
@@ -76,13 +105,17 @@ namespace UmlautAdaptarr.Models
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a german title starts with der/die/das also accept variations without it
|
||||||
|
if (mediaType != "audio" && cleanTitle.StartsWith("Der") || cleanTitle.StartsWith("Die") || cleanTitle.StartsWith("Das"))
|
||||||
|
{
|
||||||
|
var cleanTitleWithoutArticle = germanTitle[3..].Trim();
|
||||||
|
baseVariations.AddRange(GenerateVariations(cleanTitleWithoutArticle, mediaType));
|
||||||
|
}
|
||||||
|
|
||||||
// Remove multiple spaces
|
// Remove multiple spaces
|
||||||
var cleanedVariations = baseVariations.Select(variation => MultipleWhitespaceRegex().Replace(variation, " "));
|
var cleanedVariations = baseVariations.Select(variation => variation.RemoveExtraWhitespaces());
|
||||||
|
|
||||||
return cleanedVariations.Distinct();
|
return cleanedVariations.Distinct();
|
||||||
}
|
}
|
||||||
|
|
||||||
[GeneratedRegex(@"\s+")]
|
|
||||||
private static partial Regex MultipleWhitespaceRegex();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using UmlautAdaptarr.Providers;
|
using UmlautAdaptarr.Providers;
|
||||||
using UmlautAdaptarr.Routing;
|
using UmlautAdaptarr.Routing;
|
||||||
@@ -10,8 +11,6 @@ internal class Program
|
|||||||
// TODO:
|
// TODO:
|
||||||
// add option to sort by nzb age
|
// add option to sort by nzb age
|
||||||
|
|
||||||
// TODO
|
|
||||||
// add delay between requests
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -38,7 +37,7 @@ internal class Program
|
|||||||
builder.Logging.AddFilter((category, level) =>
|
builder.Logging.AddFilter((category, level) =>
|
||||||
{
|
{
|
||||||
// Prevent logging of HTTP request and response if the category is HttpClient
|
// Prevent logging of HTTP request and response if the category is HttpClient
|
||||||
if (category.Contains("System.Net.Http.HttpClient"))
|
if (category.Contains("System.Net.Http.HttpClient") || category.Contains("Microsoft.Extensions.Http.DefaultHttpClientFactory"))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -47,10 +46,11 @@ internal class Program
|
|||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddHostedService<ArrSyncBackgroundService>();
|
builder.Services.AddHostedService<ArrSyncBackgroundService>();
|
||||||
builder.Services.AddSingleton<TitleApiService>(); // TODO rename
|
builder.Services.AddSingleton<TitleApiService>();
|
||||||
builder.Services.AddSingleton<SearchItemLookupService>();
|
builder.Services.AddSingleton<SearchItemLookupService>();
|
||||||
builder.Services.AddSingleton<TitleMatchingService>();
|
builder.Services.AddSingleton<TitleMatchingService>();
|
||||||
builder.Services.AddSingleton<SonarrClient>();
|
builder.Services.AddSingleton<SonarrClient>();
|
||||||
|
builder.Services.AddSingleton<LidarrClient>();
|
||||||
builder.Services.AddSingleton<CacheService>();
|
builder.Services.AddSingleton<CacheService>();
|
||||||
builder.Services.AddSingleton<ProxyService>();
|
builder.Services.AddSingleton<ProxyService>();
|
||||||
|
|
||||||
|
|||||||
200
UmlautAdaptarr/Providers/LidarrClient.cs
Normal file
200
UmlautAdaptarr/Providers/LidarrClient.cs
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using UmlautAdaptarr.Models;
|
||||||
|
using UmlautAdaptarr.Services;
|
||||||
|
using UmlautAdaptarr.Utilities;
|
||||||
|
|
||||||
|
namespace UmlautAdaptarr.Providers
|
||||||
|
{
|
||||||
|
public class LidarrClient(
|
||||||
|
IHttpClientFactory clientFactory,
|
||||||
|
IConfiguration configuration,
|
||||||
|
TitleApiService titleService,
|
||||||
|
ILogger<LidarrClient> logger) : ArrClientBase()
|
||||||
|
{
|
||||||
|
private readonly string _lidarrHost = configuration.GetValue<string>("LIDARR_HOST") ?? throw new ArgumentException("LIDARR_HOST environment variable must be set");
|
||||||
|
private readonly string _lidarrApiKey = configuration.GetValue<string>("LIDARR_API_KEY") ?? throw new ArgumentException("LIDARR_API_KEY environment variable must be set");
|
||||||
|
private readonly string _mediaType = "audio";
|
||||||
|
|
||||||
|
public override async Task<IEnumerable<SearchItem>> FetchAllItemsAsync()
|
||||||
|
{
|
||||||
|
var httpClient = clientFactory.CreateClient();
|
||||||
|
var items = new List<SearchItem>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
var lidarrArtistsUrl = $"{_lidarrHost}/api/v1/artist?apikey={_lidarrApiKey}";
|
||||||
|
logger.LogInformation($"Fetching all artists from Lidarr: {UrlUtilities.RedactApiKey(lidarrArtistsUrl)}");
|
||||||
|
var artistsApiResponse = await httpClient.GetStringAsync(lidarrArtistsUrl);
|
||||||
|
var artists = JsonConvert.DeserializeObject<List<dynamic>>(artistsApiResponse);
|
||||||
|
|
||||||
|
if (artists == null)
|
||||||
|
{
|
||||||
|
logger.LogError($"Lidarr artists API request resulted in null");
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
logger.LogInformation($"Successfully fetched {artists.Count} artists from Lidarr.");
|
||||||
|
foreach (var artist in artists)
|
||||||
|
{
|
||||||
|
var artistId = (int)artist.id;
|
||||||
|
|
||||||
|
var lidarrAlbumUrl = $"{_lidarrHost}/api/v1/album?artistId={artistId}&apikey={_lidarrApiKey}";
|
||||||
|
logger.LogInformation($"Fetching all albums from artistId {artistId} from Lidarr: {UrlUtilities.RedactApiKey(lidarrArtistsUrl)}");
|
||||||
|
var albumApiResponse = await httpClient.GetStringAsync(lidarrAlbumUrl);
|
||||||
|
var albums = JsonConvert.DeserializeObject<List<dynamic>>(albumApiResponse);
|
||||||
|
|
||||||
|
if (albums == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning($"Lidarr album API request for artistId {artistId} resulted in null");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation($"Successfully fetched {albums.Count} albums for artistId {artistId} from Lidarr.");
|
||||||
|
|
||||||
|
foreach (var album in albums)
|
||||||
|
{
|
||||||
|
var artistName = (string)album.artist.artistName;
|
||||||
|
var albumTitle = (string)album.title;
|
||||||
|
|
||||||
|
var expectedTitle = $"{artistName} {albumTitle}";
|
||||||
|
|
||||||
|
string[]? aliases = null;
|
||||||
|
|
||||||
|
// Abuse externalId to set the search string Lidarr uses
|
||||||
|
var externalId = expectedTitle.RemoveGermanUmlautDots().RemoveAccent().RemoveSpecialCharacters().RemoveExtraWhitespaces().ToLower();
|
||||||
|
|
||||||
|
var searchItem = new SearchItem
|
||||||
|
(
|
||||||
|
arrId: artistId,
|
||||||
|
externalId: externalId,
|
||||||
|
title: albumTitle,
|
||||||
|
expectedTitle: albumTitle,
|
||||||
|
germanTitle: null,
|
||||||
|
aliases: aliases,
|
||||||
|
mediaType: _mediaType,
|
||||||
|
expectedAuthor: artistName
|
||||||
|
);
|
||||||
|
|
||||||
|
items.Add(searchItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation($"Finished fetching all items from Lidarr");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError($"Error fetching all artists from Lidarr: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<SearchItem?> FetchItemByExternalIdAsync(string externalId)
|
||||||
|
{
|
||||||
|
var httpClient = clientFactory.CreateClient();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var lidarrUrl = $"{_lidarrHost}/api/v1/series?mbId={externalId}&includeSeasonImages=false&apikey={_lidarrApiKey}";
|
||||||
|
logger.LogInformation($"Fetching item by external ID from Lidarr: {UrlUtilities.RedactApiKey(lidarrUrl)}");
|
||||||
|
var response = await httpClient.GetStringAsync(lidarrUrl);
|
||||||
|
var artists = JsonConvert.DeserializeObject<dynamic>(response);
|
||||||
|
var artist = artists?[0];
|
||||||
|
|
||||||
|
if (artist != null)
|
||||||
|
{
|
||||||
|
var mbId = (string)artist.mbId;
|
||||||
|
if (mbId == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning($"Lidarr Artist {artist.id} doesn't have a mbId.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
(var germanTitle, var aliases) = await titleService.FetchGermanTitleAndAliasesByExternalIdAsync(_mediaType, mbId);
|
||||||
|
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
var searchItem = new SearchItem
|
||||||
|
(
|
||||||
|
arrId: (int)artist.id,
|
||||||
|
externalId: mbId,
|
||||||
|
title: (string)artist.title,
|
||||||
|
expectedTitle: (string)artist.title,
|
||||||
|
germanTitle: germanTitle,
|
||||||
|
aliases: aliases,
|
||||||
|
mediaType: _mediaType,
|
||||||
|
expectedAuthor: "TODO"
|
||||||
|
); ;
|
||||||
|
|
||||||
|
logger.LogInformation($"Successfully fetched artist {searchItem} from Lidarr.");
|
||||||
|
return searchItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError($"Error fetching single artist from Lidarr: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<SearchItem?> FetchItemByTitleAsync(string title)
|
||||||
|
{
|
||||||
|
var httpClient = clientFactory.CreateClient();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(string? germanTitle, string? mbId, string[]? aliases) = await titleService.FetchGermanTitleAndExternalIdAndAliasesByTitle(_mediaType, title);
|
||||||
|
|
||||||
|
if (mbId == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lidarrUrl = $"{_lidarrHost}/api/v1/series?mbId={mbId}&includeSeasonImages=false&apikey={_lidarrApiKey}";
|
||||||
|
var lidarrApiResponse = await httpClient.GetStringAsync(lidarrUrl);
|
||||||
|
var artists = JsonConvert.DeserializeObject<dynamic>(lidarrApiResponse);
|
||||||
|
|
||||||
|
if (artists == null)
|
||||||
|
{
|
||||||
|
logger.LogError($"Parsing Lidarr API response for MB ID {mbId} resulted in null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else if (artists.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning($"No results found for MB ID {mbId}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedTitle = (string)artists[0].title;
|
||||||
|
if (expectedTitle == null)
|
||||||
|
{
|
||||||
|
logger.LogError($"Lidarr Title for MB ID {mbId} is null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotImplementedException();
|
||||||
|
var searchItem = new SearchItem
|
||||||
|
(
|
||||||
|
arrId: (int)artists[0].id,
|
||||||
|
externalId: mbId,
|
||||||
|
title: (string)artists[0].title,
|
||||||
|
expectedTitle: (string)artists[0].title,
|
||||||
|
germanTitle: germanTitle,
|
||||||
|
aliases: aliases,
|
||||||
|
mediaType: _mediaType,
|
||||||
|
expectedAuthor: "TODO"
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.LogInformation($"Successfully fetched artist {searchItem} from Lidarr.");
|
||||||
|
return searchItem;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError($"Error fetching single artist from Lidarr: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System.Net.Http;
|
|
||||||
using UmlautAdaptarr.Models;
|
using UmlautAdaptarr.Models;
|
||||||
using UmlautAdaptarr.Services;
|
using UmlautAdaptarr.Services;
|
||||||
using UmlautAdaptarr.Utilities;
|
using UmlautAdaptarr.Utilities;
|
||||||
|
|||||||
@@ -13,9 +13,13 @@ namespace UmlautAdaptarr.Services
|
|||||||
{
|
{
|
||||||
public class ArrSyncBackgroundService(
|
public class ArrSyncBackgroundService(
|
||||||
SonarrClient sonarrClient,
|
SonarrClient sonarrClient,
|
||||||
|
LidarrClient lidarrClient,
|
||||||
CacheService cacheService,
|
CacheService cacheService,
|
||||||
|
IConfiguration configuration,
|
||||||
ILogger<ArrSyncBackgroundService> logger) : BackgroundService
|
ILogger<ArrSyncBackgroundService> logger) : BackgroundService
|
||||||
{
|
{
|
||||||
|
private readonly bool _sonarrEnabled = configuration.GetValue<bool>("SONARR_ENABLED");
|
||||||
|
private readonly bool _lidarrEnabled = configuration.GetValue<bool>("LIDARR_ENABLED");
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
logger.LogInformation("ArrSyncBackgroundService is starting.");
|
logger.LogInformation("ArrSyncBackgroundService is starting.");
|
||||||
@@ -23,43 +27,78 @@ namespace UmlautAdaptarr.Services
|
|||||||
while (!stoppingToken.IsCancellationRequested)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
logger.LogInformation("ArrSyncBackgroundService is running.");
|
logger.LogInformation("ArrSyncBackgroundService is running.");
|
||||||
await FetchAndUpdateDataAsync();
|
var syncSuccess = await FetchAndUpdateDataAsync();
|
||||||
logger.LogInformation("ArrSyncBackgroundService has completed an iteration.");
|
logger.LogInformation("ArrSyncBackgroundService has completed an iteration.");
|
||||||
|
|
||||||
|
if (syncSuccess)
|
||||||
|
{
|
||||||
await Task.Delay(TimeSpan.FromHours(12), stoppingToken);
|
await Task.Delay(TimeSpan.FromHours(12), stoppingToken);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogInformation("ArrSyncBackgroundService is sleeping for one hour only because not all syncs were successful.");
|
||||||
|
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.LogInformation("ArrSyncBackgroundService is stopping.");
|
logger.LogInformation("ArrSyncBackgroundService is stopping.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FetchAndUpdateDataAsync()
|
private async Task<bool> FetchAndUpdateDataAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await FetchItemsFromSonarrAsync();
|
var success = true;
|
||||||
|
if (_sonarrEnabled)
|
||||||
|
{
|
||||||
|
success = await FetchItemsFromSonarrAsync();
|
||||||
|
}
|
||||||
|
if (_lidarrEnabled)
|
||||||
|
{
|
||||||
|
success = await FetchItemsFromLidarrAsync();
|
||||||
|
}
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "An error occurred while fetching items from the Arrs.");
|
logger.LogError(ex, "An error occurred while fetching items from the Arrs.");
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FetchItemsFromSonarrAsync()
|
private async Task<bool> FetchItemsFromSonarrAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var items = await sonarrClient.FetchAllItemsAsync();
|
var items = await sonarrClient.FetchAllItemsAsync();
|
||||||
UpdateSearchItems(items);
|
UpdateSearchItems(items);
|
||||||
|
return items?.Any()?? false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "An error occurred while updating search item from Sonarr.");
|
logger.LogError(ex, "An error occurred while updating search item from Sonarr.");
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateSearchItems(IEnumerable<SearchItem> searchItems)
|
private async Task<bool> FetchItemsFromLidarrAsync()
|
||||||
{
|
{
|
||||||
foreach (var searchItem in searchItems)
|
try
|
||||||
|
{
|
||||||
|
var items = await lidarrClient.FetchAllItemsAsync();
|
||||||
|
UpdateSearchItems(items);
|
||||||
|
return items?.Any() ?? false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "An error occurred while updating search item from Lidarr.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSearchItems(IEnumerable<SearchItem>? searchItems)
|
||||||
|
{
|
||||||
|
foreach (var searchItem in searchItems ?? [])
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,20 +1,28 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using UmlautAdaptarr.Models;
|
using UmlautAdaptarr.Models;
|
||||||
using UmlautAdaptarr.Utilities;
|
using UmlautAdaptarr.Utilities;
|
||||||
|
|
||||||
namespace UmlautAdaptarr.Services
|
namespace UmlautAdaptarr.Services
|
||||||
{
|
{
|
||||||
public class CacheService(IMemoryCache cache)
|
public partial class CacheService(IMemoryCache cache)
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, HashSet<string>> VariationIndex = [];
|
private readonly Dictionary<string, HashSet<string>> VariationIndex = [];
|
||||||
|
private readonly Dictionary<string, List<SearchItem>> AudioFuzzyIndex = [];
|
||||||
private const int VARIATION_LOOKUP_CACHE_LENGTH = 5;
|
private const int VARIATION_LOOKUP_CACHE_LENGTH = 5;
|
||||||
|
|
||||||
public void CacheSearchItem(SearchItem item)
|
public void CacheSearchItem(SearchItem item)
|
||||||
{
|
{
|
||||||
var prefix = item.MediaType;
|
var prefix = item.MediaType;
|
||||||
|
cache.Set($"{prefix}_extid_{item.ExternalId}", item);
|
||||||
|
if (item.MediaType == "audio")
|
||||||
|
{
|
||||||
|
CacheAudioSearchItem(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var normalizedTitle = item.Title.RemoveAccentButKeepGermanUmlauts().ToLower();
|
var normalizedTitle = item.Title.RemoveAccentButKeepGermanUmlauts().ToLower();
|
||||||
|
|
||||||
cache.Set($"{prefix}_extid_{item.ExternalId}", item);
|
|
||||||
cache.Set($"{prefix}_title_{normalizedTitle}", item);
|
cache.Set($"{prefix}_title_{normalizedTitle}", item);
|
||||||
|
|
||||||
foreach (var variation in item.TitleMatchVariations)
|
foreach (var variation in item.TitleMatchVariations)
|
||||||
@@ -33,6 +41,26 @@ namespace UmlautAdaptarr.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CacheAudioSearchItem(SearchItem item)
|
||||||
|
{
|
||||||
|
// Normalize and simplify the title and author for fuzzy matching
|
||||||
|
var key = NormalizeForFuzzyMatching(item.ExternalId);
|
||||||
|
|
||||||
|
if (!AudioFuzzyIndex.ContainsKey(key))
|
||||||
|
{
|
||||||
|
AudioFuzzyIndex[key] = new List<SearchItem>();
|
||||||
|
}
|
||||||
|
AudioFuzzyIndex[key].Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NormalizeForFuzzyMatching(string input)
|
||||||
|
{
|
||||||
|
// Normalize the input string by removing accents, converting to lower case, and removing non-alphanumeric characters
|
||||||
|
var normalized = input.RemoveAccentButKeepGermanUmlauts().RemoveSpecialCharacters().ToLower();
|
||||||
|
normalized = WhiteSpaceRegex().Replace(normalized, "");
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
public SearchItem? SearchItemByTitle(string mediaType, string title)
|
public SearchItem? SearchItemByTitle(string mediaType, string title)
|
||||||
{
|
{
|
||||||
var normalizedTitle = title.RemoveAccentButKeepGermanUmlauts().ToLower();
|
var normalizedTitle = title.RemoveAccentButKeepGermanUmlauts().ToLower();
|
||||||
@@ -90,5 +118,8 @@ namespace UmlautAdaptarr.Services
|
|||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex("\\s")]
|
||||||
|
private static partial Regex WhiteSpaceRegex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ using UmlautAdaptarr.Providers;
|
|||||||
|
|
||||||
namespace UmlautAdaptarr.Services
|
namespace UmlautAdaptarr.Services
|
||||||
{
|
{
|
||||||
public class SearchItemLookupService(CacheService cacheService, SonarrClient sonarrClient)
|
public class SearchItemLookupService(CacheService cacheService, SonarrClient sonarrClient, LidarrClient lidarrClient, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
|
private readonly bool _sonarrEnabled = configuration.GetValue<bool>("SONARR_ENABLED");
|
||||||
|
private readonly bool _lidarrEnabled = configuration.GetValue<bool>("LIDARR_ENABLED");
|
||||||
public async Task<SearchItem?> GetOrFetchSearchItemByExternalId(string mediaType, string externalId)
|
public async Task<SearchItem?> GetOrFetchSearchItemByExternalId(string mediaType, string externalId)
|
||||||
{
|
{
|
||||||
// Attempt to get the item from the cache first
|
// Attempt to get the item from the cache first
|
||||||
@@ -19,9 +21,17 @@ namespace UmlautAdaptarr.Services
|
|||||||
switch (mediaType)
|
switch (mediaType)
|
||||||
{
|
{
|
||||||
case "tv":
|
case "tv":
|
||||||
|
if (_sonarrEnabled)
|
||||||
|
{
|
||||||
fetchedItem = await sonarrClient.FetchItemByExternalIdAsync(externalId);
|
fetchedItem = await sonarrClient.FetchItemByExternalIdAsync(externalId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "audio":
|
||||||
|
if (_lidarrEnabled)
|
||||||
|
{
|
||||||
|
fetchedItem = await lidarrClient.FetchItemByExternalIdAsync(externalId);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
// TODO Add cases for other sources like Radarr, Lidarr, etc.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If an item is fetched, cache it
|
// If an item is fetched, cache it
|
||||||
@@ -47,7 +57,10 @@ namespace UmlautAdaptarr.Services
|
|||||||
switch (mediaType)
|
switch (mediaType)
|
||||||
{
|
{
|
||||||
case "tv":
|
case "tv":
|
||||||
|
if (_sonarrEnabled)
|
||||||
|
{
|
||||||
fetchedItem = await sonarrClient.FetchItemByTitleAsync(title);
|
fetchedItem = await sonarrClient.FetchItemByTitleAsync(title);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
// TODO add cases for other sources as needed, such as Radarr, Lidarr, etc.
|
// TODO add cases for other sources as needed, such as Radarr, Lidarr, etc.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
using UmlautAdaptarr.Models;
|
||||||
using UmlautAdaptarr.Utilities;
|
using UmlautAdaptarr.Utilities;
|
||||||
|
|
||||||
namespace UmlautAdaptarr.Services
|
namespace UmlautAdaptarr.Services
|
||||||
{
|
{
|
||||||
public partial class TitleMatchingService(CacheService cacheService, ILogger<TitleMatchingService> logger)
|
public partial class TitleMatchingService(CacheService cacheService, ILogger<TitleMatchingService> logger)
|
||||||
{
|
{
|
||||||
public string RenameTitlesInContent(string content, string[]? titleMatchVariations, string? expectedTitle)
|
public string RenameTitlesInContent(string content, SearchItem? searchItem)
|
||||||
{
|
{
|
||||||
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 = searchItem == null;
|
||||||
bool useCacheService = string.IsNullOrEmpty(expectedTitle) || titleMatchVariations?.Length == 0;
|
|
||||||
|
|
||||||
foreach (var item in xDoc.Descendants("item"))
|
foreach (var item in xDoc.Descendants("item"))
|
||||||
{
|
{
|
||||||
@@ -21,31 +21,143 @@ namespace UmlautAdaptarr.Services
|
|||||||
var originalTitle = titleElement.Value;
|
var originalTitle = titleElement.Value;
|
||||||
var normalizedOriginalTitle = NormalizeTitle(originalTitle);
|
var normalizedOriginalTitle = NormalizeTitle(originalTitle);
|
||||||
|
|
||||||
if (useCacheService)
|
|
||||||
{
|
|
||||||
var categoryElement = item.Element("category");
|
var categoryElement = item.Element("category");
|
||||||
var category = categoryElement?.Value;
|
var category = categoryElement?.Value;
|
||||||
var mediaType = GetMediaTypeFromCategory(category);
|
var mediaType = GetMediaTypeFromCategory(category);
|
||||||
|
|
||||||
if (mediaType == null)
|
if (mediaType == null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use CacheService to find a matching SearchItem by title
|
if (useCacheService)
|
||||||
var searchItem = cacheService.SearchItemByTitle(mediaType, normalizedOriginalTitle);
|
|
||||||
if (searchItem != null)
|
|
||||||
{
|
{
|
||||||
// If a SearchItem is found, use its ExpectedTitle and titleMatchVariations for renaming
|
// Use CacheService to find a matching SearchItem by title
|
||||||
expectedTitle = searchItem.ExpectedTitle;
|
searchItem = cacheService.SearchItemByTitle(mediaType, normalizedOriginalTitle);
|
||||||
titleMatchVariations = searchItem.TitleMatchVariations;
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (searchItem == null)
|
||||||
{
|
{
|
||||||
// Skip processing this item if no matching SearchItem is found
|
// Skip processing this item if no matching SearchItem is found
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (mediaType)
|
||||||
|
{
|
||||||
|
case "tv":
|
||||||
|
FindAndReplaceForMoviesAndTV(logger, searchItem, titleElement, originalTitle, normalizedOriginalTitle!);
|
||||||
|
break;
|
||||||
|
case "movie":
|
||||||
|
FindAndReplaceForMoviesAndTV(logger, searchItem, titleElement, originalTitle, normalizedOriginalTitle!);
|
||||||
|
break;
|
||||||
|
case "audio":
|
||||||
|
ReplaceForAudio(searchItem, titleElement, originalTitle, normalizedOriginalTitle!);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return xDoc.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NormalizeString(string text)
|
||||||
|
{
|
||||||
|
return text.RemoveGermanUmlautDots().RemoveAccent().RemoveSpecialCharacters().Replace(" ", "").Trim().ToLower();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void ReplaceForAudio(SearchItem searchItem, XElement? titleElement, string originalTitle, string normalizedOriginalTitle)
|
||||||
|
{
|
||||||
|
var authorMatch = FindBestMatch(searchItem.AuthorMatchVariations, NormalizeString(normalizedOriginalTitle), originalTitle);
|
||||||
|
var titleMatch = FindBestMatch(searchItem.TitleMatchVariations, NormalizeString(normalizedOriginalTitle), originalTitle);
|
||||||
|
|
||||||
|
if (authorMatch.Item1 && titleMatch.Item1)
|
||||||
|
{
|
||||||
|
int matchEndPositionInOriginal = Math.Max(authorMatch.Item3, titleMatch.Item3);
|
||||||
|
|
||||||
|
// Ensure we trim any leading delimiters from the suffix
|
||||||
|
string suffix = originalTitle.Substring(matchEndPositionInOriginal).TrimStart([' ', '-', '_']);
|
||||||
|
|
||||||
|
// Concatenate the expected title with the remaining suffix
|
||||||
|
var updatedTitle = $"{searchItem.ExpectedAuthor} - {searchItem.ExpectedTitle}-{suffix}";
|
||||||
|
|
||||||
|
// Update the title element
|
||||||
|
titleElement.Value = updatedTitle;
|
||||||
|
logger.LogInformation($"TitleMatchingService - Title changed: '{originalTitle}' to '{updatedTitle}'");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogInformation("TitleMatchingService - No satisfactory fuzzy match found for both author and title.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Tuple<bool, int, int> FindBestMatch(string[] variations, string normalizedOriginal, string originalTitle)
|
||||||
|
{
|
||||||
|
bool found = false;
|
||||||
|
int bestStart = int.MaxValue;
|
||||||
|
int bestEndInOriginal = -1;
|
||||||
|
|
||||||
|
foreach (var variation in variations)
|
||||||
|
{
|
||||||
|
var normalizedVariation = NormalizeString(variation);
|
||||||
|
int startNormalized = normalizedOriginal.IndexOf(normalizedVariation);
|
||||||
|
|
||||||
|
if (startNormalized >= 0)
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
// Map the start position from the normalized string back to the original string
|
||||||
|
int startOriginal = MapNormalizedIndexToOriginal(normalizedOriginal, originalTitle, startNormalized);
|
||||||
|
int endOriginal = MapNormalizedIndexToOriginal(normalizedOriginal, originalTitle, startNormalized + normalizedVariation.Length);
|
||||||
|
|
||||||
|
bestStart = Math.Min(bestStart, startOriginal);
|
||||||
|
bestEndInOriginal = Math.Max(bestEndInOriginal, endOriginal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) return Tuple.Create(false, 0, 0);
|
||||||
|
return Tuple.Create(found, bestStart, bestEndInOriginal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps an index from the normalized string back to a corresponding index in the original string
|
||||||
|
private int MapNormalizedIndexToOriginal(string normalizedOriginal, string originalTitle, int normalizedIndex)
|
||||||
|
{
|
||||||
|
// Count non-special characters up to the given index in the normalized string
|
||||||
|
int nonSpecialCharCount = 0;
|
||||||
|
for (int i = 0; i < normalizedIndex && i < normalizedOriginal.Length; i++)
|
||||||
|
{
|
||||||
|
if (char.IsLetterOrDigit(normalizedOriginal[i]))
|
||||||
|
{
|
||||||
|
nonSpecialCharCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count non-special characters in the original title to find the corresponding index
|
||||||
|
int originalIndex = 0;
|
||||||
|
for (int i = 0; i < originalTitle.Length; i++)
|
||||||
|
{
|
||||||
|
if (char.IsLetterOrDigit(originalTitle[i]))
|
||||||
|
{
|
||||||
|
if (--nonSpecialCharCount < 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
originalIndex = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalIndex + 1; // +1 to move past the matched character or to the next character in the original title
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// This method replaces the first variation that starts at the beginning of the release title
|
||||||
|
private static void FindAndReplaceForMoviesAndTV(ILogger<TitleMatchingService> logger, SearchItem searchItem, XElement? titleElement, string originalTitle, string normalizedOriginalTitle)
|
||||||
|
{
|
||||||
|
var titleMatchVariations = searchItem.TitleMatchVariations;
|
||||||
|
var expectedTitle = searchItem.ExpectedTitle;
|
||||||
var variationsOrderedByLength = titleMatchVariations!.OrderByDescending(variation => variation.Length);
|
var variationsOrderedByLength = titleMatchVariations!.OrderByDescending(variation => variation.Length);
|
||||||
// Attempt to find a variation that matches the start of the original title
|
// Attempt to find a variation that matches the start of the original title
|
||||||
foreach (var variation in variationsOrderedByLength)
|
foreach (var variation in variationsOrderedByLength)
|
||||||
@@ -105,11 +217,6 @@ namespace UmlautAdaptarr.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return xDoc.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static string NormalizeTitle(string title)
|
private static string NormalizeTitle(string title)
|
||||||
{
|
{
|
||||||
@@ -126,7 +233,11 @@ namespace UmlautAdaptarr.Services
|
|||||||
|
|
||||||
private static string ReconstructTitleWithSeparator(string title, char separator)
|
private static string ReconstructTitleWithSeparator(string title, char separator)
|
||||||
{
|
{
|
||||||
// Replace spaces with the original separator found in the title
|
if (separator != ' ')
|
||||||
|
{
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
return title.Replace(' ', separator);
|
return title.Replace(' ', separator);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +264,10 @@ namespace UmlautAdaptarr.Services
|
|||||||
{
|
{
|
||||||
return "book";
|
return "book";
|
||||||
}
|
}
|
||||||
|
else if (category.StartsWith("Audio"))
|
||||||
|
{
|
||||||
|
return "audio";
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -160,5 +275,6 @@ namespace UmlautAdaptarr.Services
|
|||||||
|
|
||||||
[GeneratedRegex("[._ ]")]
|
[GeneratedRegex("[._ ]")]
|
||||||
private static partial Regex WordSeperationCharRegex();
|
private static partial Regex WordSeperationCharRegex();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ namespace UmlautAdaptarr.Services
|
|||||||
germanTitle = titleApiResponseData.germanTitle;
|
germanTitle = titleApiResponseData.germanTitle;
|
||||||
hasGermanTitle = true;
|
hasGermanTitle = true;
|
||||||
|
|
||||||
var hasGermanUmlaut = germanTitle?.HasGermanUmlauts() ?? false;
|
var hasGermanUmlaut = germanTitle?.HasUmlauts() ?? false;
|
||||||
|
|
||||||
var result = (hasGermanUmlaut, germanTitle, expectedTitle);
|
var result = (hasGermanUmlaut, germanTitle, expectedTitle);
|
||||||
memoryCache.Set(cacheKey, result, new MemoryCacheEntryOptions
|
memoryCache.Set(cacheKey, result, new MemoryCacheEntryOptions
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace UmlautAdaptarr.Utilities
|
namespace UmlautAdaptarr.Utilities
|
||||||
{
|
{
|
||||||
public static class Extensions
|
public static partial class Extensions
|
||||||
{
|
{
|
||||||
public static string GetQuery(this HttpContext context, string key)
|
public static string GetQuery(this HttpContext context, string key)
|
||||||
{
|
{
|
||||||
@@ -46,11 +47,18 @@ namespace UmlautAdaptarr.Utilities
|
|||||||
return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
|
return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO possibly replace GetCleanTitle with RemoveSpecialCharacters
|
||||||
public static string GetCleanTitle(this string text)
|
public static string GetCleanTitle(this string text)
|
||||||
{
|
{
|
||||||
return text.Replace("(", "").Replace(")", "").Replace("?","").Replace(":", "").Replace("'", "");
|
return text.Replace("(", "").Replace(")", "").Replace("?","").Replace(":", "").Replace("'", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string RemoveSpecialCharacters(this string text)
|
||||||
|
{
|
||||||
|
return SpecialCharactersRegex().Replace(text, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static string ReplaceGermanUmlautsWithLatinEquivalents(this string text)
|
public static string ReplaceGermanUmlautsWithLatinEquivalents(this string text)
|
||||||
{
|
{
|
||||||
return text
|
return text
|
||||||
@@ -75,11 +83,22 @@ namespace UmlautAdaptarr.Utilities
|
|||||||
.Replace("ß", "ss");
|
.Replace("ß", "ss");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool HasGermanUmlauts(this string text)
|
public static string RemoveExtraWhitespaces(this string text)
|
||||||
|
{
|
||||||
|
return MultipleWhitespaceRegex().Replace(text, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasUmlauts(this string text)
|
||||||
{
|
{
|
||||||
if (text == null) return false;
|
if (text == null) return false;
|
||||||
var umlauts = new[] { 'ö', 'ä', 'ü', 'Ä', 'Ü', 'Ö', 'ß' };
|
var umlauts = new[] { 'ö', 'ä', 'ü', 'Ä', 'Ü', 'Ö', 'ß' };
|
||||||
return umlauts.Any(text.Contains);
|
return umlauts.Any(text.Contains);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex("[^a-zA-Z0-9 ]+", RegexOptions.Compiled)]
|
||||||
|
private static partial Regex SpecialCharactersRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\s+")]
|
||||||
|
private static partial Regex MultipleWhitespaceRegex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
UmlautAdaptarr/libman.json
Normal file
5
UmlautAdaptarr/libman.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"defaultProvider": "cdnjs",
|
||||||
|
"libraries": []
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"SONARR_ENABLED": false,
|
||||||
"SONARR_HOST": "http://localhost:8989",
|
"SONARR_HOST": "http://localhost:8989",
|
||||||
"SONARR_API_KEY": ""
|
"SONARR_API_KEY": "",
|
||||||
|
"LIDARR_ENABLED": false,
|
||||||
|
"LIDARR_HOST": "http://localhost:8686",
|
||||||
|
"LIDARR_API_KEY": ""
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,17 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Berlin
|
- TZ=Europe/Berlin
|
||||||
- SONARR_HOST="http://sonarr:8989"
|
- SONARR_ENABLED=false
|
||||||
|
- SONARR_HOST="http://localhost:8989"
|
||||||
- SONARR_API_KEY="API_KEY"
|
- SONARR_API_KEY="API_KEY"
|
||||||
|
- RADARR_ENABLED=false
|
||||||
|
- RADARR_HOST="http://localhost:7878"
|
||||||
|
- RADARR_API_KEY="API_KEY"
|
||||||
|
- READARR_ENABLED=false
|
||||||
|
- READARR_HOST="http://localhost:8787"
|
||||||
|
- READARR_API_KEY="API_KEY"
|
||||||
|
- LIDARR_ENABLED=false
|
||||||
|
- LIDARR_HOST="http://localhost:8686"
|
||||||
|
- LIDARR_API_KEY="API_KEY"
|
||||||
ports:
|
ports:
|
||||||
- "5005:5005"
|
- "5005:5005"
|
||||||
|
|||||||
Reference in New Issue
Block a user