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")]
|
[HttpGet("{chapterId}/book-info")]
|
||||||
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
|
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
|
||||||
{
|
{
|
||||||
// PERF: Write this in one DB call - This does not meet NFR
|
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||||
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 bookTitle = string.Empty;
|
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;
|
bookTitle = book.Title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Ok(new BookInfoDto()
|
||||||
return new BookInfoDto()
|
|
||||||
{
|
{
|
||||||
|
ChapterNumber = dto.ChapterNumber,
|
||||||
|
VolumeNumber = dto.VolumeNumber,
|
||||||
|
VolumeId = dto.VolumeId,
|
||||||
BookTitle = bookTitle,
|
BookTitle = bookTitle,
|
||||||
VolumeId = chapter.VolumeId,
|
SeriesName = dto.SeriesName,
|
||||||
SeriesFormat = series.Format,
|
SeriesFormat = dto.SeriesFormat,
|
||||||
SeriesId = series.Id,
|
SeriesId = dto.SeriesId,
|
||||||
LibraryId = series.LibraryId,
|
LibraryId = dto.LibraryId,
|
||||||
};
|
IsSpecial = dto.IsSpecial,
|
||||||
|
Pages = dto.Pages,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{chapterId}/book-resources")]
|
[HttpGet("{chapterId}/book-resources")]
|
||||||
|
@ -76,34 +76,29 @@ namespace API.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.
|
/// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seriesId">Not used</param>
|
|
||||||
/// <param name="chapterId"></param>
|
/// <param name="chapterId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("chapter-info")]
|
[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);
|
var chapter = await _cacheService.Ensure(chapterId);
|
||||||
if (chapter == null) return BadRequest("Could not find Chapter");
|
if (chapter == null) return BadRequest("Could not find Chapter");
|
||||||
|
|
||||||
var volume = await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(chapter.VolumeId);
|
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||||
if (volume == null) return BadRequest("Could not find Volume");
|
|
||||||
var mangaFile = (await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId)).First();
|
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()
|
return Ok(new ChapterInfoDto()
|
||||||
{
|
{
|
||||||
ChapterNumber = chapter.Range,
|
ChapterNumber = dto.ChapterNumber,
|
||||||
VolumeNumber = volume.Number + string.Empty,
|
VolumeNumber = dto.VolumeNumber,
|
||||||
VolumeId = volume.Id,
|
VolumeId = dto.VolumeId,
|
||||||
FileName = Path.GetFileName(mangaFile.FilePath),
|
FileName = Path.GetFileName(mangaFile.FilePath),
|
||||||
SeriesName = series.Name,
|
SeriesName = dto.SeriesName,
|
||||||
SeriesFormat = series.Format,
|
SeriesFormat = dto.SeriesFormat,
|
||||||
SeriesId = series.Id,
|
SeriesId = dto.SeriesId,
|
||||||
LibraryId = series.LibraryId,
|
LibraryId = dto.LibraryId,
|
||||||
IsSpecial = chapter.IsSpecial,
|
IsSpecial = dto.IsSpecial,
|
||||||
Pages = chapter.Pages,
|
Pages = dto.Pages,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,12 +2,17 @@
|
|||||||
|
|
||||||
namespace API.DTOs.Reader
|
namespace API.DTOs.Reader
|
||||||
{
|
{
|
||||||
public class BookInfoDto
|
public class BookInfoDto : IChapterInfoDto
|
||||||
{
|
{
|
||||||
public string BookTitle { get; set; }
|
public string BookTitle { get; set; }
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
public int VolumeId { get; set; }
|
public int VolumeId { get; set; }
|
||||||
public MangaFormat SeriesFormat { 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 LibraryId { get; set; }
|
||||||
|
public int Pages { get; set; }
|
||||||
|
public bool IsSpecial { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace API.DTOs.Reader
|
namespace API.DTOs.Reader
|
||||||
{
|
{
|
||||||
public class ChapterInfoDto
|
public class ChapterInfoDto : IChapterInfoDto
|
||||||
{
|
{
|
||||||
|
|
||||||
public string ChapterNumber { get; set; }
|
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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs.Reader;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Interfaces.Repositories;
|
using API.Interfaces.Repositories;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -30,5 +31,47 @@ namespace API.Data.Repositories
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move over Chapter based queries here
|
// 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.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs.Reader;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
|
||||||
namespace API.Interfaces.Repositories
|
namespace API.Interfaces.Repositories
|
||||||
@ -8,5 +9,6 @@ namespace API.Interfaces.Repositories
|
|||||||
{
|
{
|
||||||
void Update(Chapter chapter);
|
void Update(Chapter chapter);
|
||||||
Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds);
|
Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds);
|
||||||
|
Task<IChapterInfoDto> GetChapterInfoDtoAsync(int chapterId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ namespace API.Services.Tasks
|
|||||||
/// Name of the Tag
|
/// Name of the Tag
|
||||||
/// <example>v0.4.3</example>
|
/// <example>v0.4.3</example>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
public string Tag_Name { get; init; }
|
public string Tag_Name { get; init; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Name of the Release
|
/// Name of the Release
|
||||||
@ -35,6 +36,7 @@ namespace API.Services.Tasks
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Url of the release on Github
|
/// Url of the release on Github
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
public string Html_Url { get; init; }
|
public string Html_Url { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,8 +55,10 @@ namespace API.Services.Tasks
|
|||||||
private readonly IHubContext<MessageHub> _messageHub;
|
private readonly IHubContext<MessageHub> _messageHub;
|
||||||
private readonly IPresenceTracker _tracker;
|
private readonly IPresenceTracker _tracker;
|
||||||
private readonly Markdown _markdown = new MarkdownDeep.Markdown();
|
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 GithubLatestReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases/latest";
|
||||||
private static readonly string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases";
|
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)
|
public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IHubContext<MessageHub> messageHub, IPresenceTracker tracker)
|
||||||
{
|
{
|
||||||
@ -95,7 +99,7 @@ namespace API.Services.Tasks
|
|||||||
|
|
||||||
if (updateVersion.Revision == -1)
|
if (updateVersion.Revision == -1)
|
||||||
{
|
{
|
||||||
currentVersion = currentVersion.Substring(0, currentVersion.LastIndexOf("."));
|
currentVersion = currentVersion.Substring(0, currentVersion.LastIndexOf(".", StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new UpdateNotificationDto()
|
return new UpdateNotificationDto()
|
||||||
|
@ -56,8 +56,8 @@ export class ReaderService {
|
|||||||
return this.baseUrl + 'reader/image?chapterId=' + chapterId + '&page=' + page;
|
return this.baseUrl + 'reader/image?chapterId=' + chapterId + '&page=' + page;
|
||||||
}
|
}
|
||||||
|
|
||||||
getChapterInfo(seriesId: number, chapterId: number) {
|
getChapterInfo(chapterId: number) {
|
||||||
return this.httpClient.get<ChapterInfo>(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId + '&seriesId=' + seriesId);
|
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) {
|
saveProgress(seriesId: number, volumeId: number, chapterId: number, page: number, bookScrollId: string | null = null) {
|
||||||
|
@ -63,11 +63,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row no-gutters">
|
<div class="row no-gutters">
|
||||||
<div class="col-1">{{pageNum}}</div>
|
<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-10" style="margin-top: 9px">
|
<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>
|
<ngb-progressbar style="cursor: pointer" title="Go to page" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
|
||||||
</div>
|
</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>
|
||||||
<div class="table-of-contents">
|
<div class="table-of-contents">
|
||||||
<h3>Table of Contents</h3>
|
<h3>Table of Contents</h3>
|
||||||
|
@ -378,7 +378,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
forkJoin({
|
forkJoin({
|
||||||
progress: this.readerService.getProgress(this.chapterId),
|
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)
|
bookmarks: this.readerService.getBookmarks(this.chapterId)
|
||||||
}).pipe(take(1)).subscribe(results => {
|
}).pipe(take(1)).subscribe(results => {
|
||||||
|
|
||||||
@ -867,13 +867,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (this.pageNum >= this.maxPages - 10) {
|
if (this.pageNum >= this.maxPages - 10) {
|
||||||
// Tell server to cache the next chapter
|
// Tell server to cache the next chapter
|
||||||
if (this.nextChapterId > 0 && !this.nextChapterPrefetched) {
|
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;
|
this.nextChapterPrefetched = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (this.pageNum <= 10) {
|
} else if (this.pageNum <= 10) {
|
||||||
if (this.prevChapterId > 0 && !this.prevChapterPrefetched) {
|
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;
|
this.prevChapterPrefetched = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user