Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a67d5c2d1e | ||
|
|
d1d05f8264 | ||
|
|
939b902be3 | ||
|
|
f56d071642 | ||
|
|
7513e7d227 | ||
|
|
7a791dab23 | ||
|
|
402a4deba3 | ||
|
|
d31508fef3 | ||
|
|
1c329e886d | ||
|
|
6932c4b2f8 | ||
|
|
ff051569ca | ||
|
|
dbac09bf36 | ||
|
|
0bde5d5d24 | ||
|
|
99af842fc6 | ||
|
|
fbfbeadb3e | ||
|
|
7cfae00511 | ||
|
|
cac920ae88 | ||
|
|
bab60771a4 | ||
|
|
828faae486 | ||
|
|
333a18ecd5 |
16
README.md
16
README.md
@@ -5,7 +5,7 @@
|
|||||||
## Erste Testversion
|
## Erste Testversion
|
||||||
Wer möchte kann den UmlautAdaptarr jetzt gerne testen! Über Feedback würde ich mich sehr freuen!
|
Wer möchte kann den UmlautAdaptarr jetzt gerne testen! Über Feedback würde ich mich sehr freuen!
|
||||||
|
|
||||||
Es sollte mit allen *arrs funktionieren, hat aber nur bei Sonarr und Lidarr schon Auswirkungen (abgesehen vom Caching).
|
Es sollte mit allen *arrs funktionieren, hat aber nur bei Sonarr, Readarr und Lidarr schon Auswirkungen (abgesehen vom Caching).
|
||||||
|
|
||||||
Momentan ist docker dafür nötig, wer kein Docker nutzt muss sich noch etwas gedulden.
|
Momentan ist docker dafür nötig, wer kein Docker nutzt muss sich noch etwas gedulden.
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ UmlautAdaptarr löst mehrere Probleme:
|
|||||||
|
|
||||||
# Wie macht UmlautAdaptarr das?
|
# Wie macht UmlautAdaptarr das?
|
||||||
UmlautAdaptarr tut so, als wäre es ein Indexer. In Wahrheit schaltet sich UmlautAdaptarr aber nur zwischen die *arrs und den echten Indexer und kann somit die Suchen sowie die Ergebnisse abfangen und bearbeiten.
|
UmlautAdaptarr tut so, als wäre es ein Indexer. In Wahrheit schaltet sich UmlautAdaptarr aber nur zwischen die *arrs und den echten Indexer und kann somit die Suchen sowie die Ergebnisse abfangen und bearbeiten.
|
||||||
Am Ende werden die gefundenen Releases immer so umbenannt, das die Arrs sie einwandfrei erkennen.
|
Am Ende werden die gefundenen Releases immer so umbenannt, dass die Arrs sie einwandfrei erkennen.
|
||||||
Einige Beispiele findet ihr unter Features.
|
Einige Beispiele findet ihr unter Features.
|
||||||
|
|
||||||
|
|
||||||
@@ -38,15 +38,17 @@ Einige Beispiele findet ihr unter Features.
|
|||||||
|
|
||||||
| Feature | Status |
|
| Feature | Status |
|
||||||
|-------------------------------------------------------------------|---------------|
|
|-------------------------------------------------------------------|---------------|
|
||||||
| Prowlarr Support | ✓|
|
| Prowlarr & NZB Hydra Support | ✓|
|
||||||
| Sonarr Support | ✓ |
|
| Sonarr Support | ✓ |
|
||||||
| Lidarr Support | ✓|
|
| Lidarr Support | ✓|
|
||||||
|
| Readarr Support | ✓ |
|
||||||
| Releases mit deutschem Titel werden erkannt | ✓ |
|
| Releases mit deutschem Titel werden erkannt | ✓ |
|
||||||
| Releases mit TVDB-Alias Titel werden erkannt | ✓ |
|
| Releases mit TVDB-Alias Titel werden erkannt | ✓ |
|
||||||
| Korrekte Suche und Erkennung von Titel mit Umlauten | ✓ |
|
| Korrekte Suche und Erkennung von Titel mit Umlauten | ✓ |
|
||||||
| Anfragen-Caching für 5 Minuten zur Reduzierung der API-Zugriffe | ✓ |
|
| Anfragen-Caching für 5 Minuten zur Reduzierung der API-Zugriffe | ✓ |
|
||||||
|
| Usenet (newznab) Support |✓|
|
||||||
|
| Torrent Support | Vorerst nicht geplant |
|
||||||
| Radarr Support | Geplant |
|
| Radarr Support | Geplant |
|
||||||
| Readarr Support | Geplant |
|
|
||||||
| Prowlarr Unterstützung für "DE" SceneNZBs Kategorien | Geplant |
|
| Prowlarr Unterstützung für "DE" SceneNZBs Kategorien | Geplant |
|
||||||
| Unterstützung weiterer Sprachen neben Deutsch | Geplant |
|
| Unterstützung weiterer Sprachen neben Deutsch | Geplant |
|
||||||
| Wünsche? | Vorschläge? |
|
| Wünsche? | Vorschläge? |
|
||||||
@@ -87,6 +89,12 @@ Sonarr erwartet immer den Englischen Namen, der hier natürlich nicht gegeben is
|
|||||||
- [Telegram](https://t.me/pc_jones)
|
- [Telegram](https://t.me/pc_jones)
|
||||||
- Discord: pcjones1 - oder komm in den UsenetDE Discord Server: [https://discord.gg/pZrrMcJMQM](https://discord.gg/pZrrMcJMQM)
|
- Discord: pcjones1 - oder komm in den UsenetDE Discord Server: [https://discord.gg/pZrrMcJMQM](https://discord.gg/pZrrMcJMQM)
|
||||||
|
|
||||||
|
## Spenden
|
||||||
|
Über eine Spende freue ich mich natürlich immer :D
|
||||||
|
PayPal: https://paypal.me/pcjones1
|
||||||
|
|
||||||
|
Für andere Spendenmöglichkeiten gerne auf Discord oder Telegram melden - danke!
|
||||||
|
|
||||||
### Licenses & Metadata source
|
### Licenses & Metadata source
|
||||||
- TV Metadata source: https://thetvdb.com
|
- TV Metadata source: https://thetvdb.com
|
||||||
- Movie Metadata source: https://themoviedb.org
|
- Movie Metadata source: https://themoviedb.org
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
{
|
{
|
||||||
public abstract class SearchControllerBase(ProxyService proxyService, TitleMatchingService titleMatchingService) : ControllerBase
|
public abstract class SearchControllerBase(ProxyService proxyService, TitleMatchingService titleMatchingService) : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly bool TODO_FORCE_TEXT_SEARCH_ORIGINAL_TITLE = false;
|
// 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;
|
private readonly bool TODO_FORCE_TEXT_SEARCH_GERMAN_TITLE = false;
|
||||||
protected async Task<IActionResult> BaseSearch(string options,
|
protected async Task<IActionResult> BaseSearch(string options,
|
||||||
string domain,
|
string domain,
|
||||||
@@ -52,7 +53,7 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
|
|
||||||
var titleSearchVariations = new List<string>(searchItem?.TitleSearchVariations);
|
var titleSearchVariations = new List<string>(searchItem?.TitleSearchVariations);
|
||||||
|
|
||||||
string searchQuery = string.Empty;
|
var searchQuery = string.Empty;
|
||||||
if (queryParameters.TryGetValue("q", out string? q))
|
if (queryParameters.TryGetValue("q", out string? q))
|
||||||
{
|
{
|
||||||
searchQuery = q ?? string.Empty;
|
searchQuery = q ?? string.Empty;
|
||||||
@@ -185,7 +186,6 @@ namespace UmlautAdaptarr.Controllers
|
|||||||
if (categories.Split(',').Any(category => READARR_CATEGORY_IDS.Contains(category)))
|
if (categories.Split(',').Any(category => READARR_CATEGORY_IDS.Contains(category)))
|
||||||
{
|
{
|
||||||
var mediaType = "book";
|
var mediaType = "book";
|
||||||
// TODO rename function or use own
|
|
||||||
searchItem = await searchItemLookupService.GetOrFetchSearchItemByExternalId(mediaType, title.GetReadarrTitleForExternalId());
|
searchItem = await searchItemLookupService.GetOrFetchSearchItemByExternalId(mediaType, title.GetReadarrTitleForExternalId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,30 @@ namespace UmlautAdaptarr.Models
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
GenerateVariationsForTV(germanTitle, mediaType, aliases);
|
// if mediatype is movie/tv and the Expected Title ends with a year but the german title doesn't then append the year to the german title and to aliases
|
||||||
|
// example: https://thetvdb.com/series/385925-avatar-the-last-airbender -> german Title is without 2024
|
||||||
|
var yearAtEndOfTitleMatch = YearAtEndOfTitleRegex().Match(expectedTitle);
|
||||||
|
if (yearAtEndOfTitleMatch.Success)
|
||||||
|
{
|
||||||
|
string year = yearAtEndOfTitleMatch.Value[1..^1];
|
||||||
|
if (GermanTitle != null && !GermanTitle.Contains(year))
|
||||||
|
{
|
||||||
|
GermanTitle = $"{germanTitle} {year}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aliases != null)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < aliases.Length; i++)
|
||||||
|
{
|
||||||
|
if (!aliases[i].Contains(year))
|
||||||
|
{
|
||||||
|
aliases[i] = $"{aliases[i]} {year}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateVariationsForTV(GermanTitle, mediaType, aliases);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +83,12 @@ namespace UmlautAdaptarr.Models
|
|||||||
foreach (var alias in aliases)
|
foreach (var alias in aliases)
|
||||||
{
|
{
|
||||||
allTitleVariations.AddRange(GenerateVariations(alias, mediaType));
|
allTitleVariations.AddRange(GenerateVariations(alias, mediaType));
|
||||||
|
|
||||||
|
// If title contains ":" also match for "-"
|
||||||
|
if (alias.Contains(':'))
|
||||||
|
{
|
||||||
|
allTitleVariations.Add(alias.Replace(":", " -"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +108,12 @@ namespace UmlautAdaptarr.Models
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If title contains ":" also match for "-"
|
||||||
|
if (germanTitle?.Contains(':') ?? false)
|
||||||
|
{
|
||||||
|
allTitleVariations.Add(germanTitle.Replace(":", " -"));
|
||||||
|
}
|
||||||
|
|
||||||
TitleMatchVariations = allTitleVariations.Distinct(StringComparer.InvariantCultureIgnoreCase).ToArray();
|
TitleMatchVariations = allTitleVariations.Distinct(StringComparer.InvariantCultureIgnoreCase).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +158,7 @@ namespace UmlautAdaptarr.Models
|
|||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var cleanTitle = title.GetCleanTitle();
|
var cleanTitle = title.GetCleanTitle();
|
||||||
|
|
||||||
if (cleanTitle?.Length == 0)
|
if (cleanTitle?.Length == 0)
|
||||||
@@ -180,5 +216,8 @@ namespace UmlautAdaptarr.Models
|
|||||||
|
|
||||||
return cleanedVariations.Distinct();
|
return cleanedVariations.Distinct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\(\d{4}\)$")]
|
||||||
|
private static partial Regex YearAtEndOfTitleRegex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,6 @@ namespace UmlautAdaptarr.Providers
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
||||||
var sonarrUrl = $"{_sonarrHost}/api/v3/series?includeSeasonImages=false&apikey={_sonarrApiKey}";
|
var sonarrUrl = $"{_sonarrHost}/api/v3/series?includeSeasonImages=false&apikey={_sonarrApiKey}";
|
||||||
logger.LogInformation($"Fetching all items from Sonarr: {UrlUtilities.RedactApiKey(sonarrUrl)}");
|
logger.LogInformation($"Fetching all items from Sonarr: {UrlUtilities.RedactApiKey(sonarrUrl)}");
|
||||||
var response = await httpClient.GetStringAsync(sonarrUrl);
|
var response = await httpClient.GetStringAsync(sonarrUrl);
|
||||||
|
|||||||
@@ -64,15 +64,18 @@ namespace UmlautAdaptarr.Services
|
|||||||
var success = true;
|
var success = true;
|
||||||
if (_readarrEnabled)
|
if (_readarrEnabled)
|
||||||
{
|
{
|
||||||
success = success && await FetchItemsFromReadarrAsync();
|
var syncSuccess = await FetchItemsFromReadarrAsync();
|
||||||
|
success = success && syncSuccess;
|
||||||
}
|
}
|
||||||
if (_sonarrEnabled)
|
if (_sonarrEnabled)
|
||||||
{
|
{
|
||||||
success = success && await FetchItemsFromSonarrAsync();
|
var syncSuccess = await FetchItemsFromSonarrAsync();
|
||||||
|
success = success && syncSuccess;
|
||||||
}
|
}
|
||||||
if (_lidarrEnabled)
|
if (_lidarrEnabled)
|
||||||
{
|
{
|
||||||
success = success && await FetchItemsFromLidarrAsync();
|
var syncSuccess = await FetchItemsFromLidarrAsync();
|
||||||
|
success = success && syncSuccess;
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,20 @@ namespace UmlautAdaptarr.Services
|
|||||||
_cache = cache;
|
_cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task EnsureMinimumDelayAsync(string targetUri)
|
||||||
|
{
|
||||||
|
var host = new Uri(targetUri).Host;
|
||||||
|
if (_lastRequestTimes.TryGetValue(host, out var lastRequestTime))
|
||||||
|
{
|
||||||
|
var timeSinceLastRequest = DateTimeOffset.Now - lastRequestTime;
|
||||||
|
if (timeSinceLastRequest < TimeSpan.FromMilliseconds(1500))
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromMilliseconds(1500) - timeSinceLastRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_lastRequestTimes[host] = DateTimeOffset.Now;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> ProxyRequestAsync(HttpContext context, string targetUri)
|
public async Task<HttpResponseMessage> ProxyRequestAsync(HttpContext context, string targetUri)
|
||||||
{
|
{
|
||||||
if (!HttpMethods.IsGet(context.Request.Method))
|
if (!HttpMethods.IsGet(context.Request.Method))
|
||||||
@@ -27,18 +41,6 @@ namespace UmlautAdaptarr.Services
|
|||||||
throw new ArgumentException("Only GET requests are supported", context.Request.Method);
|
throw new ArgumentException("Only GET requests are supported", context.Request.Method);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throttling mechanism
|
|
||||||
var host = new Uri(targetUri).Host;
|
|
||||||
if (_lastRequestTimes.TryGetValue(host, out var lastRequestTime))
|
|
||||||
{
|
|
||||||
var timeSinceLastRequest = DateTimeOffset.Now - lastRequestTime;
|
|
||||||
if (timeSinceLastRequest < TimeSpan.FromSeconds(3))
|
|
||||||
{
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(3) - timeSinceLastRequest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_lastRequestTimes[host] = DateTimeOffset.Now;
|
|
||||||
|
|
||||||
// Check cache
|
// Check cache
|
||||||
if (_cache.TryGetValue(targetUri, out HttpResponseMessage cachedResponse))
|
if (_cache.TryGetValue(targetUri, out HttpResponseMessage cachedResponse))
|
||||||
{
|
{
|
||||||
@@ -46,6 +48,8 @@ namespace UmlautAdaptarr.Services
|
|||||||
return cachedResponse!;
|
return cachedResponse!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await EnsureMinimumDelayAsync(targetUri);
|
||||||
|
|
||||||
var requestMessage = new HttpRequestMessage
|
var requestMessage = new HttpRequestMessage
|
||||||
{
|
{
|
||||||
RequestUri = new Uri(targetUri),
|
RequestUri = new Uri(targetUri),
|
||||||
|
|||||||
@@ -35,12 +35,14 @@ namespace UmlautAdaptarr.Services
|
|||||||
if (_lidarrEnabled)
|
if (_lidarrEnabled)
|
||||||
{
|
{
|
||||||
fetchedItem = await lidarrClient.FetchItemByExternalIdAsync(externalId);
|
fetchedItem = await lidarrClient.FetchItemByExternalIdAsync(externalId);
|
||||||
|
fetchedItem = cacheService.GetSearchItemByExternalId(mediaType, externalId);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "book":
|
case "book":
|
||||||
if (_readarrEnabled)
|
if (_readarrEnabled)
|
||||||
{
|
{
|
||||||
fetchedItem = await readarrClient.FetchItemByExternalIdAsync(externalId);
|
await readarrClient.FetchItemByExternalIdAsync(externalId);
|
||||||
|
fetchedItem = cacheService.GetSearchItemByExternalId(mediaType, externalId);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ namespace UmlautAdaptarr.Services
|
|||||||
private async Task EnsureMinimumDelayAsync()
|
private async Task EnsureMinimumDelayAsync()
|
||||||
{
|
{
|
||||||
var sinceLastRequest = DateTime.Now - lastRequestTime;
|
var sinceLastRequest = DateTime.Now - lastRequestTime;
|
||||||
if (sinceLastRequest < TimeSpan.FromSeconds(2))
|
if (sinceLastRequest < TimeSpan.FromSeconds(1))
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromSeconds(2) - sinceLastRequest);
|
await Task.Delay(TimeSpan.FromSeconds(1) - sinceLastRequest);
|
||||||
}
|
}
|
||||||
lastRequestTime = DateTime.Now;
|
lastRequestTime = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO add cache, TODO add bulk request
|
||||||
public async Task<(string? germanTitle, string[]? aliases)> FetchGermanTitleAndAliasesByExternalIdAsync(string mediaType, string externalId)
|
public async Task<(string? germanTitle, string[]? aliases)> FetchGermanTitleAndAliasesByExternalIdAsync(string mediaType, string externalId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ namespace UmlautAdaptarr.Services
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Construct the new title with the original suffix
|
// Construct the new title with the original suffix
|
||||||
var newTitle = newTitlePrefix + (string.IsNullOrEmpty(suffix) ? "" : separator + suffix);
|
var newTitle = newTitlePrefix + (string.IsNullOrEmpty(suffix) ? "" : suffix.StartsWith(separator) ? suffix : $"{separator}{suffix}");
|
||||||
|
|
||||||
// Update the title element's value with the new title
|
// Update the title element's value with the new title
|
||||||
//titleElement.Value = newTitle + $"({originalTitle.Substring(0, variationLength)})";
|
//titleElement.Value = newTitle + $"({originalTitle.Substring(0, variationLength)})";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace UmlautAdaptarr.Utilities
|
|||||||
// RegEx für eine einfache URL-Validierung ohne http:// und ohne abschließenden Schrägstrich
|
// RegEx für eine einfache URL-Validierung ohne http:// und ohne abschließenden Schrägstrich
|
||||||
// Erlaubt optionale Subdomains, Domainnamen und TLDs, aber keine Pfade oder Protokolle
|
// Erlaubt optionale Subdomains, Domainnamen und TLDs, aber keine Pfade oder Protokolle
|
||||||
var regex = UrlMatchingRegex();
|
var regex = UrlMatchingRegex();
|
||||||
return regex.IsMatch(domain) && !domain.EndsWith("/");
|
return regex.IsMatch(domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string BuildUrl(string domain, IDictionary<string, string> queryParameters)
|
public static string BuildUrl(string domain, IDictionary<string, string> queryParameters)
|
||||||
|
|||||||
Reference in New Issue
Block a user