From 64ee5ee45968e5714e698ab5d1f1afb58dbb6ba9 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 8 Jul 2025 15:33:10 -0500 Subject: [PATCH] Estimated time is coded up. --- API/Controllers/BaseApiController.cs | 4 ++++ API/Controllers/BookController.cs | 11 ++--------- API/Controllers/ReaderController.cs | 14 +++++++++++--- API/DTOs/Reader/BookInfoDto.cs | 8 +------- .../Metadata/WordCountAnalyzerService.cs | 10 ++++++++-- UI/Web/src/app/_pipes/read-time-left.pipe.ts | 9 ++++----- .../book-reader/book-reader.component.html | 6 +++--- .../book-reader/book-reader.component.ts | 19 ++----------------- .../src/app/book-reader/_models/book-info.ts | 4 ---- UI/Web/src/assets/langs/en.json | 4 +++- 10 files changed, 38 insertions(+), 51 deletions(-) diff --git a/API/Controllers/BaseApiController.cs b/API/Controllers/BaseApiController.cs index 7806ef660..bee612fa2 100644 --- a/API/Controllers/BaseApiController.cs +++ b/API/Controllers/BaseApiController.cs @@ -10,4 +10,8 @@ namespace API.Controllers; [Authorize] public class BaseApiController : ControllerBase { + public BaseApiController() + { + + } } diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index e727d8c1a..75061889d 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -41,13 +41,12 @@ public class BookController : BaseApiController /// /// [HttpGet("{chapterId}/book-info")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "includeWordCounts"])] - public async Task> GetBookInfo(int chapterId, bool includeWordCounts = false) + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId"])] + public async Task> GetBookInfo(int chapterId) { var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId); if (dto == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); var bookTitle = string.Empty; - IDictionary? pageWordCounts = null; switch (dto.SeriesFormat) { @@ -57,11 +56,6 @@ public class BookController : BaseApiController using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.LenientBookReaderOptions); bookTitle = book.Title; - if (includeWordCounts) - { - // TODO: Cache this in temp/chapterId folder to avoid having to process file each time - pageWordCounts = await _bookService.GetWordCountsPerPage(mangaFile.FilePath); - } break; } case MangaFormat.Pdf: @@ -94,7 +88,6 @@ public class BookController : BaseApiController LibraryId = dto.LibraryId, IsSpecial = dto.IsSpecial, Pages = dto.Pages, - PageWordCounts = pageWordCounts }; diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 1071ba46c..178597611 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -15,6 +15,7 @@ using API.Entities.Enums; using API.Extensions; using API.Services; using API.Services.Plus; +using API.Services.Tasks.Metadata; using API.SignalR; using Hangfire; using Kavita.Common; @@ -222,7 +223,6 @@ public class ReaderController : BaseApiController /// /// Should Kavita extract pdf into images. Defaults to false. /// Include file dimensions. Only useful for image-based reading - /// Include epub word counts per page. Only useful for epub-based reading /// [HttpGet("chapter-info")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "extractPdf", "includeDimensions"])] @@ -849,10 +849,18 @@ public class ReaderController : BaseApiController // Patch in the reading progress await _unitOfWork.ChapterRepository.AddChapterModifiers(User.GetUserId(), chapter); - // TODO: We need to actually use word count from the pages if (series.Format == MangaFormat.Epub) { - var progressCount = chapter.WordCount; + // Get the word counts for all the pages + var pageCounts = await _bookService.GetWordCountsPerPage(chapter.Files.First().FilePath); // TODO: Cache + if (pageCounts == null) return _readerService.GetTimeEstimate(series.WordCount, 0, true); + + // Sum character counts only for pages that have been read + var totalCharactersRead = pageCounts + .Where(kvp => kvp.Key <= chapter.PagesRead) + .Sum(kvp => kvp.Value); + + var progressCount = WordCountAnalyzerService.GetWordCount(totalCharactersRead); var wordsLeft = series.WordCount - progressCount; return _readerService.GetTimeEstimate(wordsLeft, 0, true); } diff --git a/API/DTOs/Reader/BookInfoDto.cs b/API/DTOs/Reader/BookInfoDto.cs index 7f56ca160..2473cd5dc 100644 --- a/API/DTOs/Reader/BookInfoDto.cs +++ b/API/DTOs/Reader/BookInfoDto.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using API.Entities.Enums; +using API.Entities.Enums; namespace API.DTOs.Reader; @@ -16,9 +15,4 @@ public sealed record BookInfoDto : IChapterInfoDto public int Pages { get; set; } public bool IsSpecial { get; set; } public string ChapterTitle { get; set; } = default! ; - /// - /// For Epub reader, this will contain Page number -> word count. All other times will be null. - /// - /// This is optionally returned by includeWordCounts - public IDictionary? PageWordCounts { get; set; } } diff --git a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs index a1c1e4e02..4eaf3d278 100644 --- a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs +++ b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs @@ -247,7 +247,6 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService _unitOfWork.MangaFileRepository.Update(file); } - private async Task GetWordCountFromHtml(EpubLocalTextContentFileRef bookFile, string filePath) { try @@ -256,7 +255,8 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService doc.LoadHtml(await bookFile.ReadContentAsync()); var textNodes = doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]"); - return textNodes?.Sum(node => node.InnerText.Count(char.IsLetter)) / AverageCharactersPerWord ?? 0; + var characterCount = textNodes?.Sum(node => node.InnerText.Count(char.IsLetter)) ?? 0; + return GetWordCount(characterCount); } catch (EpubContentException ex) { @@ -267,4 +267,10 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService } } + public static int GetWordCount(int characterCount) + { + if (characterCount == 0) return 0; + return characterCount / AverageCharactersPerWord; + } + } diff --git a/UI/Web/src/app/_pipes/read-time-left.pipe.ts b/UI/Web/src/app/_pipes/read-time-left.pipe.ts index 43ac41c86..5dd04dc75 100644 --- a/UI/Web/src/app/_pipes/read-time-left.pipe.ts +++ b/UI/Web/src/app/_pipes/read-time-left.pipe.ts @@ -1,7 +1,6 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; import {TranslocoService} from "@jsverse/transloco"; import {HourEstimateRange} from "../_models/series-detail/hour-estimate-range"; -import {DecimalPipe} from "@angular/common"; @Pipe({ name: 'readTimeLeft', @@ -11,10 +10,10 @@ export class ReadTimeLeftPipe implements PipeTransform { constructor(private readonly translocoService: TranslocoService) {} - transform(readingTimeLeft: HourEstimateRange): string { + transform(readingTimeLeft: HourEstimateRange, includeLeftLabel = false): string { const hoursLabel = readingTimeLeft.avgHours > 1 - ? this.translocoService.translate('read-time-pipe.hours') - : this.translocoService.translate('read-time-pipe.hour'); + ? this.translocoService.translate(`read-time-pipe.hours${includeLeftLabel ? '-left' : ''}`) + : this.translocoService.translate(`read-time-pipe.hour${includeLeftLabel ? '-left' : ''}`); const formattedHours = this.customRound(readingTimeLeft.avgHours); diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html index b5228ec46..b21b95804 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html @@ -122,7 +122,7 @@ } -
+
@if(isLoading) { } @else { @@ -139,9 +139,9 @@ @let timeLeft = readingTimeLeftResource.value(); @if (timeLeft) { , - + - {{timeLeft! | readTimeLeft }} + {{timeLeft! | readTimeLeft:true }} } diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts index c33768b82..0d2665e12 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts @@ -246,12 +246,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { */ nextPageDisabled = false; - /** - * Internal property used to capture all the different css properties to render on all elements. This is a cached version that is updated from reader-settings component - */ - //pageStyles = model(this.readerSettingsService.getDefaultPageStyles()); - //pageStyles!: PageStyle; - /** * Offset for drawer and rendering canvas. Fixed to 62px. */ @@ -270,7 +264,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { request: () => ({ chapterId: this.chapterId, seriesId: this.seriesId, - pageNumber: this.pageNum() + pageNumber: this.pageNum(), }), loader: async ({ request }) => { return this.readerService.getTimeLeftForChapter(this.seriesId, this.chapterId).toPromise(); @@ -420,16 +414,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { return this.pageNum() === 0 && (currentVirtualPage === 0); } - - - // get VerticalBookContentWidth() { - // if (this.layoutMode() !== BookPageLayoutMode.Default && this.writingStyle() !== WritingStyle.Horizontal ) { - // const width = this.getVerticalPageWidth() - // return width + 'px'; - // } - // return ''; - // } - + get PageWidthForPagination() { if (this.layoutMode() === BookPageLayoutMode.Default && this.writingStyle() === WritingStyle.Vertical && this.horizontalScrollbarNeeded) { diff --git a/UI/Web/src/app/book-reader/_models/book-info.ts b/UI/Web/src/app/book-reader/_models/book-info.ts index e564bf84d..b0649123b 100644 --- a/UI/Web/src/app/book-reader/_models/book-info.ts +++ b/UI/Web/src/app/book-reader/_models/book-info.ts @@ -6,8 +6,4 @@ export interface BookInfo { seriesId: number; libraryId: number; volumeId: number; - /** - * Maps the page number to character count. Only available on epub reader. - */ - pageWordCounts: {[key: number]: number}; } diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index d30e01fd2..ebe03285e 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -2785,7 +2785,9 @@ "read-time-pipe": { "less-than-hour": "<1 Hour", "hour": "Hour", - "hours": "Hours" + "hours": "Hours", + "hour-left": "Hour left", + "hours-left": "Hours left" }, "metadata-setting-field-pipe": {