mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-31 04:04:19 -04:00
Performance/cache epub (#438)
Changed: (Performance) Added the ability for epubs to cache, allowing faster page load for users with network mounted storage. (Fixes Investigate caching epubs (benefit for network mounted users) #433 )
This commit is contained in:
parent
3dbe7eec1f
commit
3c9f73ce2c
@ -215,4 +215,8 @@
|
|||||||
<_ContentIncludedByDefault Remove="wwwroot\vendor.6b2a0912ae80e6fd297f.js.map" />
|
<_ContentIncludedByDefault Remove="wwwroot\vendor.6b2a0912ae80e6fd297f.js.map" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="System.Drawing.Common" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
@ -18,14 +19,16 @@ namespace API.Controllers
|
|||||||
private readonly ILogger<BookController> _logger;
|
private readonly ILogger<BookController> _logger;
|
||||||
private readonly IBookService _bookService;
|
private readonly IBookService _bookService;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly ICacheService _cacheService;
|
||||||
private static readonly string BookApiUrl = "book-resources?file=";
|
private static readonly string BookApiUrl = "book-resources?file=";
|
||||||
|
|
||||||
|
|
||||||
public BookController(ILogger<BookController> logger, IBookService bookService, IUnitOfWork unitOfWork)
|
public BookController(ILogger<BookController> logger, IBookService bookService, IUnitOfWork unitOfWork, ICacheService cacheService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_bookService = bookService;
|
_bookService = bookService;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
|
_cacheService = cacheService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{chapterId}/book-info")]
|
[HttpGet("{chapterId}/book-info")]
|
||||||
@ -169,9 +172,11 @@ namespace API.Controllers
|
|||||||
[HttpGet("{chapterId}/book-page")]
|
[HttpGet("{chapterId}/book-page")]
|
||||||
public async Task<ActionResult<string>> GetBookPage(int chapterId, [FromQuery] int page)
|
public async Task<ActionResult<string>> GetBookPage(int chapterId, [FromQuery] int page)
|
||||||
{
|
{
|
||||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
|
var chapter = await _cacheService.Ensure(chapterId);
|
||||||
|
var path = _cacheService.GetCachedEpubFile(chapter.Id, chapter);
|
||||||
|
|
||||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
|
|
||||||
|
using var book = await EpubReader.OpenBookAsync(path);
|
||||||
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
||||||
|
|
||||||
var counter = 0;
|
var counter = 0;
|
||||||
@ -196,12 +201,7 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
if (doc.ParseErrors.Any())
|
if (doc.ParseErrors.Any())
|
||||||
{
|
{
|
||||||
_logger.LogError("{FilePath} has an invalid html file (Page {PageName})", book.FilePath, contentFileRef.FileName);
|
LogBookErrors(book, contentFileRef, doc);
|
||||||
foreach (var error in doc.ParseErrors)
|
|
||||||
{
|
|
||||||
_logger.LogError("Line {LineNumber}, Reason: {Reason}", error.Line, error.Reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
return BadRequest("The file is malformed! Cannot read.");
|
return BadRequest("The file is malformed! Cannot read.");
|
||||||
}
|
}
|
||||||
_logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath);
|
_logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath);
|
||||||
@ -322,5 +322,14 @@ namespace API.Controllers
|
|||||||
|
|
||||||
return BadRequest("Could not find the appropriate html for that page");
|
return BadRequest("Could not find the appropriate html for that page");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LogBookErrors(EpubBookRef book, EpubTextContentFileRef contentFileRef, HtmlDocument doc)
|
||||||
|
{
|
||||||
|
_logger.LogError("{FilePath} has an invalid html file (Page {PageName})", book.FilePath, contentFileRef.FileName);
|
||||||
|
foreach (var error in doc.ParseErrors)
|
||||||
|
{
|
||||||
|
_logger.LogError("Line {LineNumber}, Reason: {Reason}", error.Line, error.Reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,5 +35,6 @@ namespace API.Interfaces.Services
|
|||||||
Task<(string path, MangaFile file)> GetCachedPagePath(Chapter chapter, int page);
|
Task<(string path, MangaFile file)> GetCachedPagePath(Chapter chapter, int page);
|
||||||
|
|
||||||
void EnsureCacheDirectory();
|
void EnsureCacheDirectory();
|
||||||
|
string GetCachedEpubFile(int chapterId, Chapter chapter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,6 @@ using HtmlAgilityPack;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.IO;
|
using Microsoft.IO;
|
||||||
using VersOne.Epub;
|
using VersOne.Epub;
|
||||||
using Image = NetVips.Image;
|
|
||||||
using Point = System.Drawing.Point;
|
|
||||||
|
|
||||||
namespace API.Services
|
namespace API.Services
|
||||||
{
|
{
|
||||||
@ -409,7 +407,7 @@ namespace API.Services
|
|||||||
if (!createThumbnail) return coverImageContent.ReadContent();
|
if (!createThumbnail) return coverImageContent.ReadContent();
|
||||||
|
|
||||||
using var stream = StreamManager.GetStream("BookService.GetCoverImage", coverImageContent.ReadContent());
|
using var stream = StreamManager.GetStream("BookService.GetCoverImage", coverImageContent.ReadContent());
|
||||||
using var thumbnail = Image.ThumbnailStream(stream, MetadataService.ThumbnailWidth);
|
using var thumbnail = NetVips.Image.ThumbnailStream(stream, MetadataService.ThumbnailWidth);
|
||||||
return thumbnail.WriteToBuffer(".jpg");
|
return thumbnail.WriteToBuffer(".jpg");
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -433,7 +431,7 @@ namespace API.Services
|
|||||||
|
|
||||||
if (!createThumbnail) return stream.ToArray();
|
if (!createThumbnail) return stream.ToArray();
|
||||||
|
|
||||||
using var thumbnail = Image.ThumbnailStream(stream, MetadataService.ThumbnailWidth);
|
using var thumbnail = NetVips.Image.ThumbnailStream(stream, MetadataService.ThumbnailWidth);
|
||||||
return thumbnail.WriteToBuffer(".png");
|
return thumbnail.WriteToBuffer(".png");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ using API.Entities.Enums;
|
|||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
|
using Kavita.Common;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Services
|
namespace API.Services
|
||||||
@ -42,6 +43,23 @@ namespace API.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the full path to the cached epub file. If the file does not exist, will fallback to the original.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <param name="chapter"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string GetCachedEpubFile(int chapterId, Chapter chapter)
|
||||||
|
{
|
||||||
|
var extractPath = GetCachePath(chapterId);
|
||||||
|
var path = Path.Join(extractPath, Path.GetFileName(chapter.Files.First().FilePath));
|
||||||
|
if (!(new FileInfo(path).Exists))
|
||||||
|
{
|
||||||
|
path = chapter.Files.First().FilePath;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Chapter> Ensure(int chapterId)
|
public async Task<Chapter> Ensure(int chapterId)
|
||||||
{
|
{
|
||||||
EnsureCacheDirectory();
|
EnsureCacheDirectory();
|
||||||
@ -50,6 +68,7 @@ namespace API.Services
|
|||||||
var fileCount = files.Count;
|
var fileCount = files.Count;
|
||||||
var extractPath = GetCachePath(chapterId);
|
var extractPath = GetCachePath(chapterId);
|
||||||
var extraPath = "";
|
var extraPath = "";
|
||||||
|
var removeNonImages = true;
|
||||||
|
|
||||||
if (Directory.Exists(extractPath))
|
if (Directory.Exists(extractPath))
|
||||||
{
|
{
|
||||||
@ -83,15 +102,24 @@ namespace API.Services
|
|||||||
|
|
||||||
if (file.Format == MangaFormat.Archive)
|
if (file.Format == MangaFormat.Archive)
|
||||||
{
|
{
|
||||||
_archiveService.ExtractArchive(file.FilePath, Path.Join(extractPath, extraPath));
|
_archiveService.ExtractArchive(file.FilePath, Path.Join(extractPath, extraPath));
|
||||||
} else if (file.Format == MangaFormat.Pdf)
|
} else if (file.Format == MangaFormat.Pdf)
|
||||||
{
|
{
|
||||||
_bookService.ExtractPdfImages(file.FilePath, Path.Join(extractPath, extraPath));
|
_bookService.ExtractPdfImages(file.FilePath, Path.Join(extractPath, extraPath));
|
||||||
|
} else if (file.Format == MangaFormat.Epub)
|
||||||
|
{
|
||||||
|
removeNonImages = false;
|
||||||
|
DirectoryService.ExistOrCreate(extractPath);
|
||||||
|
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extractDi.Flatten();
|
extractDi.Flatten();
|
||||||
extractDi.RemoveNonImages();
|
if (removeNonImages)
|
||||||
|
{
|
||||||
|
extractDi.RemoveNonImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return chapter;
|
return chapter;
|
||||||
}
|
}
|
||||||
|
@ -106,11 +106,18 @@ namespace API.Services
|
|||||||
|
|
||||||
public void CopyFileToDirectory(string fullFilePath, string targetDirectory)
|
public void CopyFileToDirectory(string fullFilePath, string targetDirectory)
|
||||||
{
|
{
|
||||||
var fileInfo = new FileInfo(fullFilePath);
|
try
|
||||||
if (fileInfo.Exists)
|
{
|
||||||
{
|
var fileInfo = new FileInfo(fullFilePath);
|
||||||
fileInfo.CopyTo(Path.Join(targetDirectory, fileInfo.Name));
|
if (fileInfo.Exists)
|
||||||
}
|
{
|
||||||
|
fileInfo.CopyTo(Path.Join(targetDirectory, fileInfo.Name), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "There was a critical error when copying {File} to {Directory}", fullFilePath, targetDirectory);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<Company>kavitareader.com</Company>
|
<Company>kavitareader.com</Company>
|
||||||
<Product>Kavita</Product>
|
<Product>Kavita</Product>
|
||||||
<AssemblyVersion>0.4.3.2</AssemblyVersion>
|
<AssemblyVersion>0.4.3.3</AssemblyVersion>
|
||||||
<NeutralLanguage>en</NeutralLanguage>
|
<NeutralLanguage>en</NeutralLanguage>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user