bf31effd36
Fixes #64 ### Summary of Changes 1. **Extended `IEpubReader` & `EpubReaderService`**: Added `GetEpubResourceAsync` to handle binary data extraction of static assets (like images) from the EPUB archive. 2. **Added Client-Side HTTP Call**: Extended `WasmEpubService` to retrieve static resources from the server using the API client. 3. **Preserved and Sanitized Images**: Updated `ExtractParagraphs` and `SanitizeParagraph` to treat `<img>` tags as first-class citizens, preserving their `src` attributes and excluding them from sanitization stripping. 4. **Dynamic URL Rewriting**: Introduced a relative-to-absolute path resolution algorithm (`ResolveRelativePath`) and rewrote image `src` attributes to use the dynamic endpoint `/api/epub/{ebookId}/resource?path=...`. 5. **Registered API Resource Serving Endpoint**: Added the `/api/epub/{ebookId:guid}/resource` minimal API endpoint in `Program.cs` that maps requests directly to `GetEpubResourceAsync` and returns files with the correct MIME type. 6. **Added Unit Tests**: Created `EpubReaderServiceTests.cs` to verify all image extraction, path resolution, and sanitization/rewriting rules. All tests pass successfully. --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #65 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
83 lines
3.0 KiB
C#
83 lines
3.0 KiB
C#
using System.Net.Http.Json;
|
|
using FluentResults;
|
|
using NexusReader.Application.Abstractions.Services;
|
|
using NexusReader.Application.Queries.Reader;
|
|
|
|
namespace NexusReader.Web.Client.Services;
|
|
|
|
public class WasmEpubReader : IEpubReader
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
|
|
public WasmEpubReader(HttpClient httpClient)
|
|
{
|
|
_httpClient = httpClient;
|
|
}
|
|
|
|
public async Task<Result<ReaderPageViewModel>> GetEpubContentAsync(
|
|
Guid ebookId,
|
|
int chapterIndex,
|
|
string? userId = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var response = await _httpClient.GetAsync($"/api/epub/{ebookId}/{chapterIndex}", cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var viewModel = await response.Content.ReadFromJsonAsync<ReaderPageViewModel>(cancellationToken: cancellationToken);
|
|
return viewModel != null ? Result.Ok(viewModel) : Result.Fail("Failed to deserialize response.");
|
|
}
|
|
|
|
var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
return Result.Fail($"Server error ({response.StatusCode}): {errorBody}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result.Fail(new Error($"Network or parsing error: {ex.Message}").CausedBy(ex));
|
|
}
|
|
}
|
|
public async Task<Result<byte[]>> GetEpubResourceAsync(
|
|
Guid ebookId,
|
|
string resourcePath,
|
|
string? userId = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var response = await _httpClient.GetAsync($"/api/epub/{ebookId}/resource?path={Uri.EscapeDataString(resourcePath)}", cancellationToken);
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
|
return Result.Ok(bytes);
|
|
}
|
|
|
|
var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
return Result.Fail($"Server error fetching EPUB resource ({response.StatusCode}): {errorBody}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result.Fail(new Error($"Network error fetching EPUB resource: {ex.Message}").CausedBy(ex));
|
|
}
|
|
}
|
|
}
|
|
|
|
public class WasmEpubMetadataExtractor : IEpubMetadataExtractor
|
|
{
|
|
public async Task<Result<LocalEpubMetadata>> ExtractMetadataAsync(Stream epubStream)
|
|
{
|
|
try
|
|
{
|
|
using var bookRef = await VersOne.Epub.EpubReader.OpenBookAsync(epubStream);
|
|
var title = bookRef.Title ?? "Unknown Title";
|
|
var author = bookRef.Author ?? "Unknown Author";
|
|
byte[]? cover = await bookRef.ReadCoverAsync();
|
|
return Result.Ok(new LocalEpubMetadata { Title = title, Author = author, CoverImage = cover });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result.Fail(new Error($"Failed to extract EPUB metadata locally: {ex.Message}").CausedBy(ex));
|
|
}
|
|
}
|
|
}
|