mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Performance Improvements (#568)
* Refactored the performance of GetChapter/BookInfo API to have a 10x speed improvement and to use common code, rather than duplicating code. Removed an api param that is no longer needed. * Book reader now has dedicated buttons to jump to next/prev chapter as well as through page buttons
This commit is contained in:
parent
84fa617023
commit
cb3929e499
@ -35,29 +35,28 @@ namespace API.Controllers
|
||||
[HttpGet("{chapterId}/book-info")]
|
||||
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
|
||||
{
|
||||
// PERF: Write this in one DB call - This does not meet NFR
|
||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
|
||||
var volume = await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(chapter.VolumeId);
|
||||
if (volume == null) return BadRequest("Could not find Volume");
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
if (series == null) return BadRequest("Series could not be found");
|
||||
|
||||
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||
var bookTitle = string.Empty;
|
||||
if (series.Format == MangaFormat.Epub)
|
||||
if (dto.SeriesFormat == MangaFormat.Epub)
|
||||
{
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
|
||||
var mangaFile = (await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath);
|
||||
bookTitle = book.Title;
|
||||
}
|
||||
|
||||
|
||||
return new BookInfoDto()
|
||||
return Ok(new BookInfoDto()
|
||||
{
|
||||
ChapterNumber = dto.ChapterNumber,
|
||||
VolumeNumber = dto.VolumeNumber,
|
||||
VolumeId = dto.VolumeId,
|
||||
BookTitle = bookTitle,
|
||||
VolumeId = chapter.VolumeId,
|
||||
SeriesFormat = series.Format,
|
||||
SeriesId = series.Id,
|
||||
LibraryId = series.LibraryId,
|
||||
};
|
||||
SeriesName = dto.SeriesName,
|
||||
SeriesFormat = dto.SeriesFormat,
|
||||
SeriesId = dto.SeriesId,
|
||||
LibraryId = dto.LibraryId,
|
||||
IsSpecial = dto.IsSpecial,
|
||||
Pages = dto.Pages,
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{chapterId}/book-resources")]
|
||||
|
@ -76,34 +76,29 @@ namespace API.Controllers
|
||||
/// <summary>
|
||||
/// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.
|
||||
/// </summary>
|
||||
/// <param name="seriesId">Not used</param>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter-info")]
|
||||
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int seriesId, int chapterId)
|
||||
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int chapterId)
|
||||
{
|
||||
// PERF: Write this in one DB call - This does not meet NFR
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
if (chapter == null) return BadRequest("Could not find Chapter");
|
||||
|
||||
var volume = await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(chapter.VolumeId);
|
||||
if (volume == null) return BadRequest("Could not find Volume");
|
||||
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||
var mangaFile = (await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
if (series == null) return BadRequest("Series could not be found");
|
||||
|
||||
return Ok(new ChapterInfoDto()
|
||||
{
|
||||
ChapterNumber = chapter.Range,
|
||||
VolumeNumber = volume.Number + string.Empty,
|
||||
VolumeId = volume.Id,
|
||||
ChapterNumber = dto.ChapterNumber,
|
||||
VolumeNumber = dto.VolumeNumber,
|
||||
VolumeId = dto.VolumeId,
|
||||
FileName = Path.GetFileName(mangaFile.FilePath),
|
||||
SeriesName = series.Name,
|
||||
SeriesFormat = series.Format,
|
||||
SeriesId = series.Id,
|
||||
LibraryId = series.LibraryId,
|
||||
IsSpecial = chapter.IsSpecial,
|
||||
Pages = chapter.Pages,
|
||||
SeriesName = dto.SeriesName,
|
||||
SeriesFormat = dto.SeriesFormat,
|
||||
SeriesId = dto.SeriesId,
|
||||
LibraryId = dto.LibraryId,
|
||||
IsSpecial = dto.IsSpecial,
|
||||
Pages = dto.Pages,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2,12 +2,17 @@
|
||||
|
||||
namespace API.DTOs.Reader
|
||||
{
|
||||
public class BookInfoDto
|
||||
public class BookInfoDto : IChapterInfoDto
|
||||
{
|
||||
public string BookTitle { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public int VolumeId { get; set; }
|
||||
public MangaFormat SeriesFormat { get; set; }
|
||||
public string SeriesName { get; set; }
|
||||
public string ChapterNumber { get; set; }
|
||||
public string VolumeNumber { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
public int Pages { get; set; }
|
||||
public bool IsSpecial { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace API.DTOs.Reader
|
||||
{
|
||||
public class ChapterInfoDto
|
||||
public class ChapterInfoDto : IChapterInfoDto
|
||||
{
|
||||
|
||||
public string ChapterNumber { get; set; }
|
||||
|
19
API/DTOs/Reader/IChapterInfoDto.cs
Normal file
19
API/DTOs/Reader/IChapterInfoDto.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using API.Entities.Enums;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.DTOs.Reader
|
||||
{
|
||||
public interface IChapterInfoDto
|
||||
{
|
||||
public int SeriesId { get; set; }
|
||||
public int VolumeId { get; set; }
|
||||
public MangaFormat SeriesFormat { get; set; }
|
||||
public string SeriesName { get; set; }
|
||||
public string ChapterNumber { get; set; }
|
||||
public string VolumeNumber { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
public int Pages { get; set; }
|
||||
public bool IsSpecial { get; set; }
|
||||
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Interfaces.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -30,5 +31,47 @@ namespace API.Data.Repositories
|
||||
}
|
||||
|
||||
// TODO: Move over Chapter based queries here
|
||||
|
||||
/// <summary>
|
||||
/// Populates a partial IChapterInfoDto
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<IChapterInfoDto> GetChapterInfoDtoAsync(int chapterId)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => c.Id == chapterId)
|
||||
.Join(_context.Volume, c => c.VolumeId, v => v.Id, (chapter, volume) => new
|
||||
{
|
||||
ChapterNumber = chapter.Range,
|
||||
VolumeNumber = volume.Number,
|
||||
VolumeId = volume.Id,
|
||||
chapter.IsSpecial,
|
||||
volume.SeriesId
|
||||
})
|
||||
.Join(_context.Series, data => data.SeriesId, series => series.Id, (data, series) => new
|
||||
{
|
||||
data.ChapterNumber,
|
||||
data.VolumeNumber,
|
||||
data.VolumeId,
|
||||
data.IsSpecial,
|
||||
data.SeriesId,
|
||||
SeriesFormat = series.Format,
|
||||
SeriesName = series.Name,
|
||||
series.LibraryId
|
||||
})
|
||||
.Select(data => new BookInfoDto()
|
||||
{
|
||||
ChapterNumber = data.ChapterNumber,
|
||||
VolumeNumber = data.VolumeNumber + string.Empty,
|
||||
VolumeId = data.VolumeId,
|
||||
IsSpecial = data.IsSpecial,
|
||||
SeriesId =data.SeriesId,
|
||||
SeriesFormat = data.SeriesFormat,
|
||||
SeriesName = data.SeriesName,
|
||||
LibraryId = data.LibraryId
|
||||
})
|
||||
.AsNoTracking()
|
||||
.SingleAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
|
||||
namespace API.Interfaces.Repositories
|
||||
@ -8,5 +9,6 @@ namespace API.Interfaces.Repositories
|
||||
{
|
||||
void Update(Chapter chapter);
|
||||
Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds);
|
||||
Task<IChapterInfoDto> GetChapterInfoDtoAsync(int chapterId);
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ namespace API.Services.Tasks
|
||||
/// Name of the Tag
|
||||
/// <example>v0.4.3</example>
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public string Tag_Name { get; init; }
|
||||
/// <summary>
|
||||
/// Name of the Release
|
||||
@ -35,6 +36,7 @@ namespace API.Services.Tasks
|
||||
/// <summary>
|
||||
/// Url of the release on Github
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public string Html_Url { get; init; }
|
||||
}
|
||||
|
||||
@ -53,8 +55,10 @@ namespace API.Services.Tasks
|
||||
private readonly IHubContext<MessageHub> _messageHub;
|
||||
private readonly IPresenceTracker _tracker;
|
||||
private readonly Markdown _markdown = new MarkdownDeep.Markdown();
|
||||
#pragma warning disable S1075
|
||||
private static readonly string GithubLatestReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases/latest";
|
||||
private static readonly string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases";
|
||||
#pragma warning restore S1075
|
||||
|
||||
public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IHubContext<MessageHub> messageHub, IPresenceTracker tracker)
|
||||
{
|
||||
@ -95,7 +99,7 @@ namespace API.Services.Tasks
|
||||
|
||||
if (updateVersion.Revision == -1)
|
||||
{
|
||||
currentVersion = currentVersion.Substring(0, currentVersion.LastIndexOf("."));
|
||||
currentVersion = currentVersion.Substring(0, currentVersion.LastIndexOf(".", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
return new UpdateNotificationDto()
|
||||
|
@ -56,8 +56,8 @@ export class ReaderService {
|
||||
return this.baseUrl + 'reader/image?chapterId=' + chapterId + '&page=' + page;
|
||||
}
|
||||
|
||||
getChapterInfo(seriesId: number, chapterId: number) {
|
||||
return this.httpClient.get<ChapterInfo>(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId + '&seriesId=' + seriesId);
|
||||
getChapterInfo(chapterId: number) {
|
||||
return this.httpClient.get<ChapterInfo>(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId);
|
||||
}
|
||||
|
||||
saveProgress(seriesId: number, volumeId: number, chapterId: number, page: number, bookScrollId: string | null = null) {
|
||||
|
@ -63,11 +63,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters">
|
||||
<div class="col-1">{{pageNum}}</div>
|
||||
<div class="col-10" style="margin-top: 9px">
|
||||
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
||||
<div class="col-1" style="margin-top: 6px">{{pageNum}}</div>
|
||||
<div class="col-8" style="margin-top: 15px">
|
||||
<ngb-progressbar style="cursor: pointer" title="Go to page" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
|
||||
</div>
|
||||
<div class="col-1 btn-icon" (click)="goToPage(maxPages - 1)" title="Go to last page">{{maxPages - 1}}</div>
|
||||
<div class="col-1 btn-icon" style="margin-top: 6px" (click)="goToPage(maxPages - 1)" title="Go to last page">{{maxPages - 1}}</div>
|
||||
<button class="btn btn-small btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<div class="table-of-contents">
|
||||
<h3>Table of Contents</h3>
|
||||
|
@ -378,7 +378,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
forkJoin({
|
||||
progress: this.readerService.getProgress(this.chapterId),
|
||||
chapterInfo: this.readerService.getChapterInfo(this.seriesId, this.chapterId),
|
||||
chapterInfo: this.readerService.getChapterInfo(this.chapterId),
|
||||
bookmarks: this.readerService.getBookmarks(this.chapterId)
|
||||
}).pipe(take(1)).subscribe(results => {
|
||||
|
||||
@ -867,13 +867,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.pageNum >= this.maxPages - 10) {
|
||||
// Tell server to cache the next chapter
|
||||
if (this.nextChapterId > 0 && !this.nextChapterPrefetched) {
|
||||
this.readerService.getChapterInfo(this.seriesId, this.nextChapterId).pipe(take(1)).subscribe(res => {
|
||||
this.readerService.getChapterInfo(this.nextChapterId).pipe(take(1)).subscribe(res => {
|
||||
this.nextChapterPrefetched = true;
|
||||
});
|
||||
}
|
||||
} else if (this.pageNum <= 10) {
|
||||
if (this.prevChapterId > 0 && !this.prevChapterPrefetched) {
|
||||
this.readerService.getChapterInfo(this.seriesId, this.prevChapterId).pipe(take(1)).subscribe(res => {
|
||||
this.readerService.getChapterInfo(this.prevChapterId).pipe(take(1)).subscribe(res => {
|
||||
this.prevChapterPrefetched = true;
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user