diff --git a/src/Aquifer.API/Modules/Bibles/BibleResponse.cs b/src/Aquifer.API/Modules/Bibles/BibleResponse.cs index bae5b1f4..54c8e188 100644 --- a/src/Aquifer.API/Modules/Bibles/BibleResponse.cs +++ b/src/Aquifer.API/Modules/Bibles/BibleResponse.cs @@ -1,32 +1,13 @@ namespace Aquifer.API.Modules.Bibles; -public class OldBibleBookResponse -{ - public int LanguageId { get; set; } - public string Name { get; set; } = null!; - - public IEnumerable Contents { get; set; } = - new List(); -} - -public class OldBibleBookResponseContent -{ - public int BookId { get; set; } - public string DisplayName { get; set; } = null!; - public string TextUrl { get; set; } = null!; - public object? AudioUrls { get; set; } - public int TextSize { get; set; } - public int AudioSize { get; set; } -} - public class BibleResponse { public int Id { get; set; } public string Name { get; set; } = null!; public string Abbreviation { get; set; } = null!; - public IEnumerable Books { get; set; } = - new List(); + public IEnumerable Books { get; set; } = + new List(); } public class BibleResponseBook diff --git a/src/Aquifer.API/Modules/Bibles/BiblesModule.cs b/src/Aquifer.API/Modules/Bibles/BiblesModule.cs index 8535cfef..c80e1ddb 100644 --- a/src/Aquifer.API/Modules/Bibles/BiblesModule.cs +++ b/src/Aquifer.API/Modules/Bibles/BiblesModule.cs @@ -1,5 +1,4 @@ -using Aquifer.API.Utilities; -using Aquifer.Data; +using Aquifer.Data; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.EntityFrameworkCore; @@ -9,51 +8,25 @@ public class BiblesModule : IModule { public IEndpointRouteBuilder MapEndpoints(IEndpointRouteBuilder endpoints) { - var oldGroup = endpoints.MapGroup("bibles"); - oldGroup.MapGet("language/{languageId:int}", GetBibleContentsByLanguage); - var group = endpoints.MapGroup("bible-versions"); - group.MapGet("language/{languageId:int}", GetBibleVersionsByLanguage); + group.MapGet("language/{languageId:int}", GetBibleByLanguage); group.MapGet("{bibleId:int}/books/{bookId:int}", GetBibleBookDetails); return endpoints; } - private async Task>> GetBibleContentsByLanguage(int languageId, - AquiferDbContext dbContext, - CancellationToken cancellationToken) - { - var bibles = await dbContext.Bibles.Where(x => x.LanguageId == languageId) - .Select(x => new BibleBookResponse - { - LanguageId = x.LanguageId, - Name = x.Name, - Contents = x.BibleBookContents.Select(y => new BibleBookResponseContent - { - BookId = y.BookId, - DisplayName = y.DisplayName, - TextUrl = y.TextUrl, - TextSize = y.TextSize, - AudioUrls = JsonUtilities.DefaultSerialize(y.AudioUrls), - AudioSize = y.AudioSize - }) - }).ToListAsync(cancellationToken); - - return TypedResults.Ok(bibles); - } - - private async Task>> GetBibleVersionsByLanguage(int languageId, + private async Task>> GetBibleByLanguage(int languageId, AquiferDbContext dbContext, CancellationToken cancellationToken) { var bibles = await dbContext.Bibles.Where(x => x.LanguageId == languageId) - .Select(bibleVersion => new BibleVersionResponse + .Select(bible => new BibleResponse { - Name = bibleVersion.Name, - Abbreviation = bibleVersion.Abbreviation, - Id = bibleVersion.Id, - Books = bibleVersion.BibleBookContents.OrderBy(book => book.BookId).Select(book => - new BibleVersionResponseBook + Name = bible.Name, + Abbreviation = bible.Abbreviation, + Id = bible.Id, + Books = bible.BibleBookContents.OrderBy(book => book.BookId).Select(book => + new BibleResponseBook { BookId = book.BookId, DisplayName = book.DisplayName, diff --git a/src/Aquifer.API/Modules/Passages/PassageResourcesResponse.cs b/src/Aquifer.API/Modules/Passages/PassageResourcesResponse.cs deleted file mode 100644 index ea6cade2..00000000 --- a/src/Aquifer.API/Modules/Passages/PassageResourcesResponse.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Aquifer.API.Modules.Passages; - -public class PassageResourcesResponse -{ - public int BookId => PassageStartDetails.BookId; - public int StartChapter => PassageStartDetails.Chapter; - public int EndChapter => PassageEndDetails.Chapter; - public int StartVerse => PassageStartDetails.Verse; - public int EndVerse => PassageEndDetails.Verse; - - public IEnumerable Resources { get; set; } = - new List(); - - [JsonIgnore] - public (int BookId, int Chapter, int Verse) PassageStartDetails { get; set; } - - [JsonIgnore] - public (int BookId, int Chapter, int Verse) PassageEndDetails { get; set; } -} - -public class PassageResourcesResponseResource -{ - public int Type { get; set; } - public int MediaType { get; set; } - public string EnglishLabel { get; set; } = null!; - public string? Tag { get; set; } - - public IEnumerable SupportingResources { get; set; } - = new List(); - - public PassageResourcesResponseResourceContent? Content { get; set; } -} - -public class PassageResourcesResponseResourceContent -{ - public int LanguageId { get; set; } - public string DisplayName { get; set; } = null!; - public string? Summary { get; set; } - public object? Content { get; set; } - public int ContentSize { get; set; } -} \ No newline at end of file diff --git a/src/Aquifer.API/Modules/Passages/PassageResponse.cs b/src/Aquifer.API/Modules/Passages/PassageResponse.cs index 298b472c..046e91a7 100644 --- a/src/Aquifer.API/Modules/Passages/PassageResponse.cs +++ b/src/Aquifer.API/Modules/Passages/PassageResponse.cs @@ -29,10 +29,10 @@ public class PassageResourceResponse public int ContentId { get; set; } [JsonConverter(typeof(JsonStringEnumConverter))] - public ResourceEntityType TypeValue { get; set; } + public ResourceEntityType TypeName { get; set; } [JsonConverter(typeof(JsonStringEnumConverter))] - public ResourceContentMediaType MediaTypeValue { get; set; } + public ResourceContentMediaType MediaTypeName { get; set; } public int ContentSize { get; set; } } \ No newline at end of file diff --git a/src/Aquifer.API/Modules/Passages/PassagesModule.cs b/src/Aquifer.API/Modules/Passages/PassagesModule.cs index 25995b6e..c51d19b0 100644 --- a/src/Aquifer.API/Modules/Passages/PassagesModule.cs +++ b/src/Aquifer.API/Modules/Passages/PassagesModule.cs @@ -12,63 +12,10 @@ public class PassagesModule : IModule public IEndpointRouteBuilder MapEndpoints(IEndpointRouteBuilder endpoints) { var group = endpoints.MapGroup("passages"); - group.MapGet("/resources/language/{languageId:int}", GetPassageResourcesByLanguage).WithOpenApi(o => new (o) {Deprecated = true}); group.MapGet("/language/{languageId:int}", GetPassagesByLanguage); return endpoints; } - private async Task>> GetPassageResourcesByLanguage(int languageId, - AquiferDbContext dbContext, - CancellationToken cancellationToken) - { - var passageContent = (await dbContext.Passages.Select(passage => - new PassageResourcesResponse - { - PassageStartDetails = BibleUtilities.TranslateVerseId(passage.StartVerseId), - PassageEndDetails = BibleUtilities.TranslateVerseId(passage.EndVerseId), - Resources = passage.PassageResources.Select(passageResource => - new PassageResourcesResponseResource - { - Type = (int)passageResource.Resource.Type, - MediaType = (int)passageResource.Resource.MediaType, - EnglishLabel = passageResource.Resource.EnglishLabel, - Tag = passageResource.Resource.Tag, - Content = - passageResource.Resource.ResourceContents.Select(resourceContent => - new PassageResourcesResponseResourceContent - { - LanguageId = resourceContent.LanguageId, - DisplayName = resourceContent.DisplayName, - Summary = resourceContent.Summary, - Content = JsonUtilities.DefaultSerialize(resourceContent.Content), - ContentSize = resourceContent.ContentSize - }).FirstOrDefault(content => content.LanguageId == languageId), - SupportingResources = passageResource.Resource.SupportingResources - .Where(sr => sr.Type != ResourceEntityType.TyndaleBibleDictionary) - .Select(supportingResource => new PassageResourcesResponseResource - { - Type = (int)supportingResource.Type, - MediaType = (int)supportingResource.MediaType, - EnglishLabel = supportingResource.EnglishLabel, - Tag = supportingResource.Tag, - Content = supportingResource.ResourceContents.Select(resourceContent => - new PassageResourcesResponseResourceContent - { - LanguageId = resourceContent.LanguageId, - DisplayName = resourceContent.DisplayName, - Summary = resourceContent.Summary, - Content = JsonUtilities.DefaultSerialize(resourceContent.Content), - ContentSize = resourceContent.ContentSize - }).FirstOrDefault(content => content.LanguageId == languageId) - }) - }) - }).ToListAsync(cancellationToken)) - .OrderBy(passage => passage.BookId) - .ThenBy(passage => passage.StartChapter).ThenBy(passage => passage.StartVerse).ToList(); - - return TypedResults.Ok(passageContent); - } - private async Task>> GetPassagesByLanguage(int languageId, AquiferDbContext dbContext, CancellationToken cancellationToken, @@ -91,8 +38,8 @@ private async Task>> GetPassagesByLanguage(int language { ContentId = content.Id, ContentSize = content.ContentSize, - MediaTypeValue = content.MediaType, - TypeValue = passageResource.Resource.Type + MediaTypeName = content.MediaType, + TypeName = passageResource.Resource.Type })), SupportingResources = passage.PassageResources.SelectMany(passageResource => passageResource .Resource.SupportingResources @@ -104,8 +51,8 @@ private async Task>> GetPassagesByLanguage(int language { ContentId = content.Id, ContentSize = content.ContentSize, - MediaTypeValue = content.MediaType, - TypeValue = passageResource.Resource.Type + MediaTypeName = content.MediaType, + TypeName = passageResource.Resource.Type }) ) } diff --git a/src/Aquifer.API/Modules/Resources/ResourceContentJsonSchema.cs b/src/Aquifer.API/Modules/Resources/ResourceContentJsonSchema.cs new file mode 100644 index 00000000..a0278f5d --- /dev/null +++ b/src/Aquifer.API/Modules/Resources/ResourceContentJsonSchema.cs @@ -0,0 +1,17 @@ +namespace Aquifer.API.Modules.Resources; + +public class ResourceContentUrlJsonSchema +{ + public string Url { get; set; } = null!; +} + +public class ResourceContentAudioJsonSchema +{ + public ResourceContentAudioTypeJsonSchema Webm { get; set; } = null!; + public ResourceContentAudioTypeJsonSchema Mp3 { get; set; } = null!; +} + +public class ResourceContentAudioTypeJsonSchema +{ + public string Url { get; set; } = null!; +} diff --git a/src/Aquifer.API/Modules/Resources/ResourcesModule.cs b/src/Aquifer.API/Modules/Resources/ResourcesModule.cs index bffb22c7..1caa7b91 100644 --- a/src/Aquifer.API/Modules/Resources/ResourcesModule.cs +++ b/src/Aquifer.API/Modules/Resources/ResourcesModule.cs @@ -3,6 +3,7 @@ using Aquifer.Data.Entities; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; namespace Aquifer.API.Modules.Resources; @@ -13,10 +14,93 @@ public IEndpointRouteBuilder MapEndpoints(IEndpointRouteBuilder endpoints) var group = endpoints.MapGroup("resources"); group.MapGet("content/{contentId:int}", GetResourceContentById); group.MapGet("metadata/{contentId:int}", GetResourceMetadataById); + group.MapGet("language/{languageId:int}/book/{bookId:int}", GetResourcesForBook); return endpoints; } + public class ResourceContentInfoForBook + { + public IEnumerable Chapters { get; set; } + } + + public class ResourceContentInfoForChapter + { + public int ChapterNumber { get; set; } + public IEnumerable Contents { get; set; } + } + + public class ResourceContentInfo + { + public int ContentId { get; set; } + public int ContentSize { get; set; } + public ResourceContentMediaType MediaType { get; set; } + public ResourceEntityType Type { get; set; } + } + + private async Task>> GetResourcesForBook( + int languageId, + int bookId, + AquiferDbContext dbContext, + CancellationToken cancellationToken, + [FromQuery] ResourceEntityType[]? resourceTypes = null + ) + { + var passageResourceContent = await dbContext.PassageResources + .Where(pr => (resourceTypes != null && resourceTypes.Contains(pr.Resource.Type)) && ((pr.Passage.StartVerseId > BibleUtilities.lowerBoundOfBook(bookId) && + pr.Passage.StartVerseId < BibleUtilities.upperBoundOfBook(bookId)) || (pr.Passage.EndVerseId > BibleUtilities.lowerBoundOfBook(bookId) && + pr.Passage.EndVerseId < BibleUtilities.upperBoundOfBook(bookId)))) + .SelectMany(pr => pr.Resource.ResourceContents.Where(rc => rc.LanguageId == languageId).Select(rc => new { + StartChapter = pr.Passage.StartVerseId / 1000 % 1000, + EndChapter = pr.Passage.EndVerseId / 1000 % 1000, + ContentId = rc.Id, + ContentSize = rc.ContentSize, + MediaType = rc.MediaType, + Type = pr.Resource.Type, + })) + .ToListAsync(cancellationToken); + + var verseResourceContent = await dbContext.VerseResources + .Where(vr => (resourceTypes != null && resourceTypes.Contains(vr.Resource.Type)) && vr.VerseId > BibleUtilities.lowerBoundOfBook(bookId) && + vr.VerseId < BibleUtilities.upperBoundOfBook(bookId)) + .SelectMany(vr => vr.Resource.ResourceContents.Where(rc => rc.LanguageId == languageId).Select(rc => new { + StartChapter = vr.VerseId / 1000 % 1000, + EndChapter = vr.VerseId / 1000 % 1000, + ContentId = rc.Id, + ContentSize = rc.ContentSize, + MediaType = rc.MediaType, + Type = vr.Resource.Type, + })) + .ToListAsync(cancellationToken); + + var allContent = passageResourceContent.Concat(verseResourceContent); + + var groupedContent = allContent + .SelectMany(content => Enumerable.Range(content.StartChapter, content.EndChapter - content.StartChapter + 1).Select(chapter => new { + ChapterNumber = chapter, + Content = new ResourceContentInfo { + ContentId = content.ContentId, + ContentSize = content.ContentSize, + MediaType = content.MediaType, + Type = content.Type + } + })) + .GroupBy(item => item.ChapterNumber) + .Select(g => new ResourceContentInfoForChapter { + ChapterNumber = g.Key, + Contents = g + .Select(item => item.Content) + .DistinctBy(item => item.ContentId) + }) + .OrderBy(item => item.ChapterNumber); + + return TypedResults.Ok(new List { + new ResourceContentInfoForBook { + Chapters = groupedContent + } + }); + } + private async Task, NotFound, ProblemHttpResult, RedirectHttpResult>> GetResourceContentById( int contentId, AquiferDbContext dbContext, @@ -32,18 +116,26 @@ private async Task, NotFound, ProblemHttpResult, RedirectHttp if (content.MediaType == ResourceContentMediaType.Text) { - return TypedResults.Ok(JsonUtilities.DefaultSerialize(content.Content)); + return TypedResults.Ok(JsonUtilities.DefaultDeserialize(content.Content)); } - string? url; + string? url = null; if (content.MediaType == ResourceContentMediaType.Audio) { - url = JsonUtilities.GetStringFromJsonPath(content.Content, new[] { audioType, "url" }); + var deserialized = JsonUtilities.DefaultDeserialize(content.Content); + if (deserialized != null) + { + url = audioType == "webm" ? deserialized.Webm.Url : deserialized.Mp3.Url; + } } else { - url = JsonUtilities.GetStringFromJsonPath(content.Content, new[] { "url" }); + var deserialized = JsonUtilities.DefaultDeserialize(content.Content); + if (deserialized != null) + { + url = deserialized.Url; + } } if (url != null) @@ -71,7 +163,7 @@ CancellationToken cancellationToken DisplayName = content.DisplayName, Additional = content.MediaType == ResourceContentMediaType.Text ? null - : JsonUtilities.DefaultSerialize(content.Content) + : JsonUtilities.DefaultDeserialize(content.Content) }; return TypedResults.Ok(response); diff --git a/src/Aquifer.API/Program.cs b/src/Aquifer.API/Program.cs index bd40858b..48259d35 100644 --- a/src/Aquifer.API/Program.cs +++ b/src/Aquifer.API/Program.cs @@ -15,9 +15,12 @@ options.UseSqlServer(configuration?.ConnectionStrings?.BiblioNexusDb)) .RegisterModules(); +builder.Services.AddResponseCompression(); + var app = builder.Build(); //app.UseAuth(); app.UseSwaggerWithUi(); +app.UseResponseCompression(); app.MapEndpoints(); app.Run(); \ No newline at end of file diff --git a/src/Aquifer.API/Utilities/BibleUtilities.cs b/src/Aquifer.API/Utilities/BibleUtilities.cs index da5b7089..4e8a62c0 100644 --- a/src/Aquifer.API/Utilities/BibleUtilities.cs +++ b/src/Aquifer.API/Utilities/BibleUtilities.cs @@ -10,4 +10,14 @@ public static (int bookId, int chapter, int verse) TranslateVerseId(int verseId) return (bookId, chapter, verse); } + + public static int lowerBoundOfBook(int bookId) + { + return (bookId * 1000000) + 1000000000; + } + + public static int upperBoundOfBook(int bookId) + { + return (bookId * 1000000) + 1000999999; + } } \ No newline at end of file diff --git a/src/Aquifer.API/Utilities/JsonUtilities.cs b/src/Aquifer.API/Utilities/JsonUtilities.cs index dc6b06e9..a740227a 100644 --- a/src/Aquifer.API/Utilities/JsonUtilities.cs +++ b/src/Aquifer.API/Utilities/JsonUtilities.cs @@ -4,23 +4,35 @@ namespace Aquifer.API.Utilities; public static class JsonUtilities { - public static T DefaultSerialize(string json) + public static T DefaultDeserialize(string json) { - return JsonSerializer.Deserialize(json, JsonSerializerOptions.Default)!; + return JsonSerializer.Deserialize(json, + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + })!; } - - public static object DefaultSerialize(string json) + public static object DefaultDeserialize(string json) { - return DefaultSerialize(json); + return DefaultDeserialize(json); } public static string? GetStringFromJsonPath(string json, string[] keys) { - JsonElement element = JsonDocument.Parse(json).RootElement; - foreach (var key in keys) + try + { + JsonElement element = JsonDocument.Parse(json).RootElement; + foreach (var key in keys) + { + element = element.GetProperty(key); + } + + return element.GetString(); + } + catch (KeyNotFoundException e) { - element = element.GetProperty(key); + return null; } - return element.GetString(); } } \ No newline at end of file