22 Commits

Author SHA1 Message Date
pcjones
2370153192 Clarify error message on indexer request error 2026-02-01 17:51:47 +01:00
pcjones
79ad4f9370 Merge branch 'master' into develop 2026-02-01 17:49:30 +01:00
pcjones
d70ae41951 v0.7.5 (#82)
* Add newzbay categories; add title lookup API

* Update dependencies

* Update docker compose

* Fix title lookup colon workaround

* Remove rid and tmdbid from text search queries
2025-11-18 11:01:15 +01:00
PCJones
2650d32ae2 Remove rid and tmdbid from text search queries 2025-11-18 10:56:05 +01:00
PCJones
5f87a596fb Fix title lookup colon workaround 2025-11-18 10:55:51 +01:00
PCJones
c2b200f3be Update docker compose 2025-11-18 10:55:25 +01:00
PCJones
6ef8cc3cd1 Merge branch 'master' into develop
Resolved conflict in TitleLookupController.cs by keeping the master version with colon handling fix.
This brings develop up to date with master (v0.7.4), including the November bug fixes.
2025-11-18 10:49:49 +01:00
pcjones
b94c6bc6ad Merge branch 'master' of https://github.com/PCJones/UmlautAdaptarr 2025-11-16 19:37:50 +01:00
pcjones
feae0ca309 Fix releases with ":" not working in title lookup API 2025-11-16 19:35:42 +01:00
pcjones
b828b64a22 Update run_on_seedbox.sh 2025-07-24 16:54:20 +02:00
pcjones
c48db39d04 v0.7.3 (#74)
* Add newzbay categories; add title lookup API

* Update dependencies
2025-07-22 16:33:13 +02:00
PCJones
02cf2854e1 Update dependencies 2025-07-22 16:32:31 +02:00
PCJones
22ecdaa6cd Add newzbay categories; add title lookup API 2025-07-22 16:30:30 +02:00
pcjones
9b6fe09e45 Update README.md 2025-04-29 11:01:46 +02:00
pcjones
b3329f6899 Update README.md 2025-04-28 00:16:48 +02:00
pcjones
0801dbbc1d Update README.md 2025-04-28 00:16:10 +02:00
pcjones
305b83609b Update README.md 2025-04-28 00:01:53 +02:00
pcjones
59654b92fb Update README.md 2025-04-28 00:01:30 +02:00
pcjones
ad2aa34e53 Update README.md 2025-04-14 15:31:56 +02:00
pcjones
3ec0be1194 Update README.md 2025-04-14 15:31:29 +02:00
pcjones
b9f56a08ec Update README.md 2025-04-14 15:30:28 +02:00
pcjones
288f7a4de9 Update README.md (#69) 2025-04-14 15:30:04 +02:00
11 changed files with 137 additions and 38 deletions

View File

@@ -42,9 +42,10 @@ Einige Beispiele finden sich [weiter unten](https://github.com/PCJones/UmlautAda
## Installation
Momentan ist Docker dafür nötig, wer kein Docker nutzt muss sich noch etwas gedulden. Eine Unraid-App gibt es auch, einfach nach `umlautadaptarr` suchen.
[Link zum Docker Image](https://hub.docker.com/r/pcjones/umlautadaptarr)
- [Docker](https://hub.docker.com/r/pcjones/umlautadaptarr)
- Unraid: nach `umlautadaptarr` suchen
- [Proxmox LXC (unofficial)](https://community-scripts.github.io/ProxmoxVE/scripts?id=umlautadaptarr) - appsettings.json muss nach Installation konfiguriert werden
- [Seedbox/Binary](https://github.com/PCJones/UmlautAdaptarr/blob/master/run_on_seedbox.sh)
Nicht benötigte Umgebungsvariablen, z.B. falls Readarr oder Lidarr nicht genutzt werden, können entfernt werden.
@@ -120,7 +121,7 @@ Sonarr erwartet immer den Englischen Namen, der hier natürlich nicht gegeben is
## Kontakt & Support
- Öffne gerne ein Issue auf GitHub falls du Unterstützung benötigst.
- [Telegram](https://t.me/pc_jones)
- Discord: pcjones1 - oder komm in den UsenetDE Discord Server: [https://discord.gg/pZrrMcJMQM](https://discord.gg/pZrrMcJMQM)
- [UsenetDE Discord Server](https://discord.gg/src6zcH4rr) -> #umlautadaptarr
## Spenden
Über eine Spende freue ich mich natürlich immer :D

View File

@@ -3,7 +3,6 @@ using Microsoft.Extensions.Options;
using System.Text;
using UmlautAdaptarr.Models;
using UmlautAdaptarr.Options;
using UmlautAdaptarr.Providers;
using UmlautAdaptarr.Services;
using UmlautAdaptarr.Utilities;
@@ -14,7 +13,7 @@ namespace UmlautAdaptarr.Controllers
// TODO evaluate if this should be set to true by default
private readonly bool TODO_FORCE_TEXT_SEARCH_ORIGINAL_TITLE = true;
private readonly bool TODO_FORCE_TEXT_SEARCH_GERMAN_TITLE = false;
protected async Task<IActionResult> BaseSearch(string apiKey,
protected async Task<IActionResult?> BaseSearch(string apiKey,
string domain,
IDictionary<string, string> queryParameters,
SearchItem? searchItem = null)
@@ -31,8 +30,7 @@ namespace UmlautAdaptarr.Controllers
return NotFound($"{domain} is not a valid URL.");
}
ContentResult? initialSearchResult = await PerformSingleSearchRequest(domain, queryParameters) as ContentResult;
if (initialSearchResult == null)
if (await PerformSingleSearchRequest(domain, queryParameters) is not ContentResult initialSearchResult)
{
return null;
}
@@ -58,6 +56,8 @@ namespace UmlautAdaptarr.Controllers
queryParameters.Remove("tvdbid");
queryParameters.Remove("tvmazeid");
queryParameters.Remove("imdbid");
queryParameters.Remove("rid");
queryParameters.Remove("tmdbid");
var titleSearchVariations = new List<string>(searchItem?.TitleSearchVariations);

View File

@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using UmlautAdaptarr.Options;
using UmlautAdaptarr.Services;
namespace UmlautAdaptarr.Controllers
{
[ApiController]
[Route("titlelookup/")]
public class TitleLookupController(CacheService cacheService, IOptions<GlobalOptions> options) : ControllerBase
{
GlobalOptions _options = options.Value;
[HttpGet]
public IActionResult GetOriginalTitle([FromQuery] string changedTitle)
{
if (!_options.EnableChangedTitleCache)
{
return StatusCode(501, "Set SETTINGS__EnableChangedTitleCache to true to use this endpoint.");
}
if (string.IsNullOrWhiteSpace(changedTitle))
return BadRequest("changedTitle is required.");
var originalTitle = cacheService.GetOriginalTitleFromRenamed(changedTitle);
return originalTitle != null
? Ok(new { changedTitle, originalTitle })
: NotFound($"Original title not found for '{changedTitle}'.");
}
}
}

View File

@@ -22,10 +22,17 @@
/// <summary>
/// API key for requests to the UmlautAdaptarr. Optional.
/// </summary>
public string? ApiKey { get; set; } = null;
/// <summary>
/// Proxy port for the internal UmlautAdaptarr proxy.
/// </summary>
public int ProxyPort { get; set; } = 5006;
}
/// <summary>
/// Enable or disable the cache for changed titles.
/// </summary>
public bool EnableChangedTitleCache { get; set; } = false;
}
}

View File

@@ -1,20 +1,19 @@
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Caching.Memory;
using System.Reflection.Metadata.Ecma335;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Caching.Memory;
using UmlautAdaptarr.Models;
using UmlautAdaptarr.Utilities;
namespace UmlautAdaptarr.Services
{
public partial class CacheService(IMemoryCache cache)
public partial class CacheService(IMemoryCache cache)
{
private readonly Dictionary<string, HashSet<string>> VariationIndex = [];
private readonly Dictionary<string, List<(HashSet<string> TitleVariations, string CacheKey)>> BookVariationIndex = [];
private readonly Dictionary<string, List<(HashSet<string> TitleVariations, string CacheKey)>> AudioVariationIndex = [];
private const int VARIATION_LOOKUP_CACHE_LENGTH = 5;
private const string TitleRenamePrefix = "title_rename_";
private static readonly TimeSpan TitleRenameCacheDuration = TimeSpan.FromHours(12);
public void CacheSearchItem(SearchItem item)
public void CacheSearchItem(SearchItem item)
{
var prefix = item.MediaType;
var cacheKey = $"{prefix}_extid_{item.ExternalId}";
@@ -196,8 +195,26 @@ namespace UmlautAdaptarr.Services
return null;
}
public void CacheTitleRename(string changedTitle, string originalTitle)
{
if (string.IsNullOrWhiteSpace(changedTitle) || string.IsNullOrWhiteSpace(originalTitle))
return;
[GeneratedRegex("\\s")]
private static partial Regex WhiteSpaceRegex();
}
var key = $"{TitleRenamePrefix}{changedTitle.Trim().ToLowerInvariant()}";
cache.Set(key, originalTitle, TitleRenameCacheDuration);
// If title contains ":" also add it as "-" for arr/sabnzbd compatibility
if (changedTitle.Contains(':'))
{
var altKey = $"{TitleRenamePrefix}{changedTitle.Replace(':', '-').Trim().ToLowerInvariant()}";
cache.Set(altKey, originalTitle, TitleRenameCacheDuration);
}
}
public string? GetOriginalTitleFromRenamed(string changedTitle)
{
var key = $"{TitleRenamePrefix}{changedTitle.Trim().ToLowerInvariant()}";
return cache.TryGetValue(key, out string? originalTitle) ? originalTitle : null;
}
}
}

View File

@@ -54,6 +54,7 @@ namespace UmlautAdaptarr.Services
}
await EnsureMinimumDelayAsync(targetUri);
var targetHost = new Uri(targetUri).Host;
var requestMessage = new HttpRequestMessage
{
@@ -88,7 +89,14 @@ namespace UmlautAdaptarr.Services
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error proxying request: {UrlUtilities.RedactApiKey(targetUri)}. Error: {ex.Message}");
var upstreamMessage = IsTimeoutOrReset(ex)
? $"{targetHost} timed out or reset the connection. This is an error with your indexer or network, not with UmlautAdaptarr."
: $"{targetHost} request failed. This is an error with your indexer or network, not with UmlautAdaptarr.";
_logger.LogError(ex, "{UpstreamMessage} Target: {TargetUri} Error: {ErrorMessage}",
upstreamMessage,
UrlUtilities.RedactApiKey(targetUri),
ex.Message);
var errorResponse = new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError)
{
@@ -97,5 +105,26 @@ namespace UmlautAdaptarr.Services
return errorResponse;
}
}
private static bool IsTimeoutOrReset(Exception ex)
{
if (ex is TaskCanceledException)
{
return true;
}
var current = ex;
while (current != null)
{
if (current is System.Net.Sockets.SocketException socketEx)
{
return socketEx.SocketErrorCode == System.Net.Sockets.SocketError.TimedOut
|| socketEx.SocketErrorCode == System.Net.Sockets.SocketError.ConnectionReset;
}
current = current.InnerException;
}
return false;
}
}
}

View File

@@ -1,14 +1,18 @@
using Microsoft.Extensions.FileSystemGlobbing.Internal;
using Microsoft.Extensions.Options;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using UmlautAdaptarr.Models;
using UmlautAdaptarr.Options;
using UmlautAdaptarr.Utilities;
namespace UmlautAdaptarr.Services
{
public partial class TitleMatchingService(CacheService cacheService, ILogger<TitleMatchingService> logger)
public partial class TitleMatchingService(CacheService cacheService, ILogger<TitleMatchingService> logger, IOptions<GlobalOptions> options)
{
public string RenameTitlesInContent(string content, SearchItem? searchItem)
public GlobalOptions _options { get; } = options.Value;
public string RenameTitlesInContent(string content, SearchItem? searchItem)
{
var xDoc = XDocument.Parse(content);
@@ -46,10 +50,10 @@ namespace UmlautAdaptarr.Services
switch (mediaType)
{
case "tv":
FindAndReplaceForMoviesAndTV(logger, searchItem, titleElement, originalTitle, cleanTitleSeperatedBySpace!);
FindAndReplaceForMoviesAndTV(searchItem, titleElement, originalTitle, cleanTitleSeperatedBySpace!);
break;
case "movie":
FindAndReplaceForMoviesAndTV(logger, searchItem, titleElement, originalTitle, cleanTitleSeperatedBySpace!);
FindAndReplaceForMoviesAndTV(searchItem, titleElement, originalTitle, cleanTitleSeperatedBySpace!);
break;
case "audio":
FindAndReplaceForBooksAndAudio(searchItem, titleElement, originalTitle!);
@@ -94,7 +98,11 @@ namespace UmlautAdaptarr.Services
// Update the title element
titleElement.Value = updatedTitle;
logger.LogInformation($"TitleMatchingService - Title changed: '{originalTitle}' to '{updatedTitle}'");
if (_options.EnableChangedTitleCache)
{
cacheService.CacheTitleRename(updatedTitle, originalTitle);
}
logger.LogInformation($"TitleMatchingService - Title changed: '{originalTitle}' to '{updatedTitle}'");
}
else
{
@@ -161,7 +169,7 @@ namespace UmlautAdaptarr.Services
}
// 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)
private void FindAndReplaceForMoviesAndTV(SearchItem searchItem, XElement? titleElement, string originalTitle, string normalizedOriginalTitle)
{
var titleMatchVariations = searchItem.TitleMatchVariations;
var expectedTitle = searchItem.ExpectedTitle;
@@ -218,8 +226,11 @@ namespace UmlautAdaptarr.Services
// Update the title element's value with the new title
//titleElement.Value = newTitle + $"({originalTitle.Substring(0, variationLength)})";
titleElement.Value = newTitle;
logger.LogInformation($"TitleMatchingService - Title changed: '{originalTitle}' to '{newTitle}'");
if (_options.EnableChangedTitleCache)
{
cacheService.CacheTitleRename(newTitle, originalTitle);
}
logger.LogInformation($"TitleMatchingService - Title changed: '{originalTitle}' to '{newTitle}'");
break;
}
}
@@ -298,23 +309,23 @@ namespace UmlautAdaptarr.Services
return null;
}
if (category == "7000" || category.StartsWith("EBook", StringComparison.OrdinalIgnoreCase) || category.StartsWith("Book", StringComparison.OrdinalIgnoreCase))
if (category == "7000" || category.StartsWith("EBook", StringComparison.OrdinalIgnoreCase) || category.StartsWith("Book", StringComparison.OrdinalIgnoreCase) ||category.StartsWith("Bücher", StringComparison.OrdinalIgnoreCase))
{
return "book";
}
else if (category == "2000" || category.StartsWith("Movies", StringComparison.OrdinalIgnoreCase))
else if (category == "2000" || category.StartsWith("Movies", StringComparison.OrdinalIgnoreCase) || category.StartsWith("Filme", StringComparison.OrdinalIgnoreCase))
{
return "movies";
}
else if (category == "5000" || category.StartsWith("TV", StringComparison.OrdinalIgnoreCase))
else if (category == "5000" || category.StartsWith("TV", StringComparison.OrdinalIgnoreCase) || category.StartsWith("Serien", StringComparison.OrdinalIgnoreCase))
{
return "tv";
}
else if (category == "3030" || category.Contains("Audiobook", StringComparison.OrdinalIgnoreCase))
else if (category == "3030" || category.Contains("Audiobook", StringComparison.OrdinalIgnoreCase) || category.Contains("Hörbuch", StringComparison.OrdinalIgnoreCase))
{
return "book";
}
else if (category == "3000" || category.StartsWith("Audio", StringComparison.OrdinalIgnoreCase))
else if (category == "3000" || category.StartsWith("Audio", StringComparison.OrdinalIgnoreCase) || category.StartsWith("Musik", StringComparison.OrdinalIgnoreCase))
{
return "audio";
}

View File

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

View File

@@ -23,7 +23,8 @@
"UmlautAdaptarrApiHost": "https://umlautadaptarr.pcjones.de/api/v1",
"IndexerRequestsCacheDurationInMinutes": 12,
"ApiKey": null,
"ProxyPort": 5006
"ProxyPort": 5006,
"EnableChangedTitleCache": false // Set to true if you are using crowdnfo.net post processing script
},
"Sonarr": [
{

View File

@@ -34,9 +34,9 @@ services:
#- SONARR__1__APIKEY=APIKEY
### Advanced options (with default values))
#- IpLeakTest__Enabled=false
#- SETTINGS__EnableChangedTitleCache=false # Enables the changed title API under /titlelookup?changedTitle=$title - enable if you are using crowdnfo.net post processing script.
#- SETTINGS__IndexerRequestsCacheDurationInMinutes=12 # How long to cache indexer requests for. Default is 12 minutes.
#- SETTINGS__ApiKey= # API key for requests to the UmlautAdaptarr. Optional, probably only needed for seedboxes.
#- SETTINGS__ProxyPort=5006 # Proxy port for the internal UmlautAdaptarr proxy used for Prowlarr.
#- Kestrel__Endpoints__Http__Url=http://[::]:5005 # HTTP port for the UmlautAdaptarr
#- IpLeakTest__Enabled=false

View File

@@ -43,6 +43,7 @@ export LIDARR__APIKEY=APIKEY
# Advanced Options
#export IpLeakTest__Enabled=false
#export SETTINGS__IndexerRequestsCacheDurationInMinutes=12
export ASPNETCORE_CONTENTROOT="./publish"
export SETTINGS__ApiKey="apikey" # Change to something unique! Then in Prowlarr, in the proxy settings set any username and use this ApiKey as password.
export SETTINGS__ProxyPort=1234 # Port for Proxy
export Kestrel__Endpoints__Http__Url="http://[::]:1235" # Port for UmlautAdaptarr API