Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
PCJones committed Feb 19, 2024
2 parents cd70997 + f804dd7 commit a4a57d8
Show file tree
Hide file tree
Showing 13 changed files with 391 additions and 63 deletions.
15 changes: 12 additions & 3 deletions UmlautAdaptarr/Controllers/SearchController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ public class SearchController(ProxyService proxyService,
TitleMatchingService titleMatchingService,
SearchItemLookupService searchItemLookupService) : SearchControllerBase(proxyService, titleMatchingService)
{
public readonly string[] AUDIO_CATEGORY_IDS = ["3000", "3010", "3020", "3040", "3050"];
public readonly string[] LIDARR_CATEGORY_IDS = ["3000", "3010", "3020", "3040", "3050"];
public readonly string[] READARR_CATEGORY_IDS = ["3030", "3130", "7000", "7010", "7020", "7030", "7100", "7110", "7120", "7130"];

[HttpGet]
public async Task<IActionResult> MovieSearch([FromRoute] string options, [FromRoute] string domain)
Expand All @@ -180,8 +181,16 @@ public async Task<IActionResult> GenericSearch([FromRoute] string options, [From
{
if (queryParameters.TryGetValue("cat", out string? categories) && !string.IsNullOrEmpty(categories))
{
// Search for audio
if (categories.Split(',').Any(category => AUDIO_CATEGORY_IDS.Contains(category)))
// look for (audio-)book
if (categories.Split(',').Any(category => READARR_CATEGORY_IDS.Contains(category)))
{
var mediaType = "book";
// TODO rename function or use own
searchItem = await searchItemLookupService.GetOrFetchSearchItemByExternalId(mediaType, title.GetReadarrTitleForExternalId());
}

// look for audio (lidarr)
if (searchItem == null && categories.Split(',').Any(category => LIDARR_CATEGORY_IDS.Contains(category)))
{
var mediaType = "audio";
searchItem = await searchItemLookupService.GetOrFetchSearchItemByExternalId(mediaType, title.GetLidarrTitleForExternalId());
Expand Down
105 changes: 68 additions & 37 deletions UmlautAdaptarr/Models/SearchItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,58 +37,83 @@ public SearchItem(
ExpectedAuthor = expectedAuthor;
GermanTitle = germanTitle;
MediaType = mediaType;
if (mediaType == "audio" && expectedAuthor != null)
if ((mediaType == "audio" || mediaType == "book") && expectedAuthor != null)
{
// e.g. Die Ärzte - best of die Ärzte
if (expectedTitle.Contains(expectedAuthor))
{
var titleWithoutAuthorName = expectedTitle.Replace(expectedAuthor, string.Empty).RemoveExtraWhitespaces().Trim();
GenerateVariationsForBooksAndAudio(expectedTitle, mediaType, expectedAuthor);
}
else
{
GenerateVariationsForTV(germanTitle, mediaType, aliases);
}
}

if (titleWithoutAuthorName.Length < 2)
{
// TODO log warning that this album can't be searched for automatically
}
TitleMatchVariations = GenerateVariations(titleWithoutAuthorName, mediaType).ToArray();
}
else
private void GenerateVariationsForTV(string? germanTitle, string mediaType, string[]? aliases)
{
TitleSearchVariations = GenerateVariations(germanTitle, mediaType).ToArray();

var allTitleVariations = new List<string>(TitleSearchVariations);

// If aliases are not null, generate variations for each and add them to the list
// TODO (not necessarily here) only use deu and eng alias
if (aliases != null)
{
foreach (var alias in aliases)
{
TitleMatchVariations = GenerateVariations(expectedTitle, mediaType).ToArray();
allTitleVariations.AddRange(GenerateVariations(alias, mediaType));
}
TitleSearchVariations = GenerateVariations($"{expectedAuthor} {expectedTitle}", mediaType).ToArray();
AuthorMatchVariations = GenerateVariations(expectedAuthor, mediaType).ToArray();
}
else

AuthorMatchVariations = [];

// if a german title ends with (DE) also add a search string that replaces (DE) with GERMAN
// also add a matching title without (DE)
if (germanTitle?.EndsWith("(DE)") ?? false)
{
TitleSearchVariations = GenerateVariations(germanTitle, mediaType).ToArray();
TitleSearchVariations = [.. TitleSearchVariations,
..
GenerateVariations(
germanTitle.Replace("(DE)", " GERMAN").RemoveExtraWhitespaces(),
mediaType)];

allTitleVariations.AddRange(GenerateVariations(germanTitle.Replace("(DE)", "").Trim(), mediaType));

}

TitleMatchVariations = allTitleVariations.Distinct(StringComparer.InvariantCultureIgnoreCase).ToArray();
}

var allTitleVariations = new List<string>(TitleSearchVariations);
private void GenerateVariationsForBooksAndAudio(string expectedTitle, string mediaType, string? expectedAuthor)
{
// e.g. Die Ärzte - best of die Ärzte
if (expectedTitle.Contains(expectedAuthor))
{
var titleWithoutAuthorName = expectedTitle.Replace(expectedAuthor, string.Empty).RemoveExtraWhitespaces().Trim();

// If aliases are not null, generate variations for each and add them to the list
// TODO (not necessarily here) only use deu and eng alias
if (aliases != null)
if (titleWithoutAuthorName.Length < 2)
{
foreach (var alias in aliases)
{
allTitleVariations.AddRange(GenerateVariations(alias, mediaType));
}
// TODO log warning that this album can't be searched for automatically
}
TitleMatchVariations = GenerateVariations(titleWithoutAuthorName, mediaType).ToArray();
}
else
{
TitleMatchVariations = GenerateVariations(expectedTitle, mediaType).ToArray();
}

AuthorMatchVariations = [];
TitleSearchVariations = GenerateVariations($"{expectedAuthor} {expectedTitle}", mediaType).ToArray();
AuthorMatchVariations = GenerateVariations(expectedAuthor, mediaType).ToArray();

// if a german title ends with (DE) also add a search string that replaces (DE) with GERMAN
// also add a matching title without (DE)
if (germanTitle?.EndsWith("(DE)") ?? false)
if (mediaType == "book")
{
if (expectedAuthor?.Contains(' ') ?? false)
{
TitleSearchVariations = [.. TitleSearchVariations, ..
GenerateVariations(
germanTitle.Replace("(DE)", " GERMAN").RemoveExtraWhitespaces(),
mediaType)];

allTitleVariations.AddRange(GenerateVariations(germanTitle.Replace("(DE)", "").Trim(), mediaType));
var nameParts = expectedAuthor.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var lastName = nameParts.Last();
var firstNames = nameParts.Take(nameParts.Length - 1);

var alternativeExpectedAuthor = $"{lastName}, {string.Join(" ", firstNames)}";
AuthorMatchVariations = [.. AuthorMatchVariations, .. GenerateVariations(alternativeExpectedAuthor, mediaType)];
}

TitleMatchVariations = allTitleVariations.Distinct(StringComparer.InvariantCultureIgnoreCase).ToArray();
}
}

Expand All @@ -113,6 +138,11 @@ private IEnumerable<string> GenerateVariations(string? title, string mediaType)
cleanTitle.RemoveGermanUmlautDots()
};

if (mediaType == "book" || mediaType == "audio")
{
baseVariations.Add(cleanTitle.RemoveGermanUmlauts());
}

// TODO: determine if this is really needed
// Additional variations to accommodate titles with "-"
if (cleanTitle.Contains('-'))
Expand Down Expand Up @@ -142,6 +172,7 @@ private IEnumerable<string> GenerateVariations(string? title, string mediaType)
} else if (cleanTitle.StartsWith("A "))
{
var cleanTitleWithoutArticle = title[2..].Trim();
baseVariations.AddRange(GenerateVariations(cleanTitleWithoutArticle, mediaType));
}

// Remove multiple spaces
Expand Down
1 change: 1 addition & 0 deletions UmlautAdaptarr/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ private static void Main(string[] args)
builder.Services.AddSingleton<TitleMatchingService>();
builder.Services.AddSingleton<SonarrClient>();
builder.Services.AddSingleton<LidarrClient>();
builder.Services.AddSingleton<ReadarrClient>();
builder.Services.AddSingleton<CacheService>();
builder.Services.AddSingleton<ProxyService>();

Expand Down
4 changes: 3 additions & 1 deletion UmlautAdaptarr/Providers/LidarrClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,15 @@ public override async Task<IEnumerable<SearchItem>> FetchAllItemsAsync()

var lidarrAlbumUrl = $"{_lidarrHost}/api/v1/album?artistId={artistId}&apikey={_lidarrApiKey}";

// TODO add caching here
// Disable cache for now as it can result in problems when adding new albums that aren't displayed on the artists page initially
//if (cache.TryGetValue(lidarrAlbumUrl, out List<dynamic>? albums))
//{
// logger.LogInformation($"Using cached albums for {UrlUtilities.RedactApiKey(lidarrAlbumUrl)}");
//}
//else
//{
logger.LogInformation($"Fetching all albums from artistId {artistId} from Lidarr: {UrlUtilities.RedactApiKey(lidarrArtistsUrl)}");
logger.LogInformation($"Fetching all albums from artistId {artistId} from Lidarr: {UrlUtilities.RedactApiKey(lidarrAlbumUrl)}");
var albumApiResponse = await httpClient.GetStringAsync(lidarrAlbumUrl);
var albums = JsonConvert.DeserializeObject<List<dynamic>>(albumApiResponse);
//}
Expand Down Expand Up @@ -108,6 +109,7 @@ public override async Task<IEnumerable<SearchItem>> FetchAllItemsAsync()
try
{
// For now we have to fetch all items every time
// TODO if possible look at the author in search query and only update for author
var searchItems = await FetchAllItemsAsync();
foreach (var searchItem in searchItems ?? [])
{
Expand Down
172 changes: 172 additions & 0 deletions UmlautAdaptarr/Providers/ReadarrClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UmlautAdaptarr.Models;
using UmlautAdaptarr.Services;
using UmlautAdaptarr.Utilities;

namespace UmlautAdaptarr.Providers
{
public class ReadarrClient(
IHttpClientFactory clientFactory,
IConfiguration configuration,
CacheService cacheService,
IMemoryCache cache,
ILogger<ReadarrClient> logger) : ArrClientBase()
{
private readonly string _readarrHost = configuration.GetValue<string>("READARR_HOST") ?? throw new ArgumentException("READARR_HOST environment variable must be set");
private readonly string _readarrApiKey = configuration.GetValue<string>("READARR_API_KEY") ?? throw new ArgumentException("READARR_API_KEY environment variable must be set");
private readonly string _mediaType = "book";

public override async Task<IEnumerable<SearchItem>> FetchAllItemsAsync()
{
var httpClient = clientFactory.CreateClient();
var items = new List<SearchItem>();

try
{
var readarrAuthorUrl = $"{_readarrHost}/api/v1/author?apikey={_readarrApiKey}";
logger.LogInformation($"Fetching all authors from Readarr: {UrlUtilities.RedactApiKey(readarrAuthorUrl)}");
var authorApiResponse = await httpClient.GetStringAsync(readarrAuthorUrl);
var authors = JsonConvert.DeserializeObject<List<dynamic>>(authorApiResponse);

if (authors == null)
{
logger.LogError($"Readarr authors API request resulted in null");
return items;
}
logger.LogInformation($"Successfully fetched {authors.Count} authors from Readarr.");
foreach (var author in authors)
{
var authorId = (int)author.id;

var readarrBookUrl = $"{_readarrHost}/api/v1/book?authorId={authorId}&apikey={_readarrApiKey}";

// TODO add caching here
logger.LogInformation($"Fetching all books from authorId {authorId} from Readarr: {UrlUtilities.RedactApiKey(readarrBookUrl)}");
var bookApiResponse = await httpClient.GetStringAsync(readarrBookUrl);
var books = JsonConvert.DeserializeObject<List<dynamic>>(bookApiResponse);

if (books == null)
{
logger.LogWarning($"Readarr book API request for authorId {authorId} resulted in null");
continue;
}

logger.LogInformation($"Successfully fetched {books.Count} books for authorId {authorId} from Readarr.");

// Cache books for 3 minutes
cache.Set(readarrBookUrl, books, TimeSpan.FromMinutes(3));

foreach (var book in books)
{
var authorName = (string)author.authorName;
var bookTitle = GetSearchBookTitle((string)book.title, authorName);

var expectedTitle = $"{bookTitle} {authorName}";

string[]? aliases = null;

// Abuse externalId to set the search string Readarr uses
// TODO use own method or rename
var externalId = expectedTitle.GetReadarrTitleForExternalId();

var searchItem = new SearchItem
(
arrId: authorId,
externalId: externalId,
title: bookTitle,
expectedTitle: bookTitle,
germanTitle: null,
aliases: aliases,
mediaType: _mediaType,
expectedAuthor: authorName
);

items.Add(searchItem);
}
}

logger.LogInformation($"Finished fetching all items from Readarr");
}
catch (Exception ex)
{
logger.LogError($"Error fetching all authors from Readarr: {ex.Message}");
}

return items;
}

// Logic based on https://github.com/Readarr/Readarr/blob/develop/src/NzbDrone.Core/Parser/Parser.cs#L541
public static string GetSearchBookTitle(string bookTitle, string authorName)
{
// Remove author prefix from title if present, e.g., "Tom Clancy: Ghost Protocol"
if (!string.IsNullOrEmpty(authorName) && bookTitle.StartsWith($"{authorName}:"))
{
bookTitle = bookTitle[(authorName.Length + 1)..].Trim();
}

// Remove subtitles or additional info enclosed in parentheses or following a colon, if any
int firstParenthesisIndex = bookTitle.IndexOf('(');
int firstColonIndex = bookTitle.IndexOf(':');

if (firstParenthesisIndex > -1)
{
int endParenthesisIndex = bookTitle.IndexOf(')', firstParenthesisIndex);
if (endParenthesisIndex > -1 && bookTitle.Substring(firstParenthesisIndex + 1, endParenthesisIndex - firstParenthesisIndex - 1).Contains(' '))
{
bookTitle = bookTitle[..firstParenthesisIndex].Trim();
}
}
else if (firstColonIndex > -1)
{
bookTitle = bookTitle[..firstColonIndex].Trim();
}

return bookTitle;
}


public override async Task<SearchItem?> FetchItemByExternalIdAsync(string externalId)
{
try
{
// For now we have to fetch all items every time
// TODO if possible look at the author in search query and only update for author
var searchItems = await FetchAllItemsAsync();
foreach (var searchItem in searchItems ?? [])
{
try
{
cacheService.CacheSearchItem(searchItem);
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred while caching search item with ID {searchItem.ArrId}.");
}
}
}
catch (Exception ex)
{
logger.LogError($"Error fetching single author from Readarr: {ex.Message}");
}

return null;
}

public override async Task<SearchItem?> FetchItemByTitleAsync(string title)
{
try
{
// this should never be called at the moment
throw new NotImplementedException();
}
catch (Exception ex)
{
logger.LogError($"Error fetching single author from Readarr: {ex.Message}");
}

return null;
}
}
}
Loading

0 comments on commit a4a57d8

Please sign in to comment.